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/feature_flag_spec.rb5
-rw-r--r--spec/config/application_spec.rb64
-rw-r--r--spec/config/settings_spec.rb36
-rw-r--r--spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json3
-rw-r--r--spec/controllers/admin/application_settings/appearances_controller_spec.rb14
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb2
-rw-r--r--spec/controllers/admin/instance_review_controller_spec.rb2
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb70
-rw-r--r--spec/controllers/admin/users_controller_spec.rb35
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb10
-rw-r--r--spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb21
-rw-r--r--spec/controllers/concerns/product_analytics_tracking_spec.rb16
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb15
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard_controller_spec.rb107
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb2
-rw-r--r--spec/controllers/graphql_controller_spec.rb129
-rw-r--r--spec/controllers/groups/children_controller_spec.rb2
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb2
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb2
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb1
-rw-r--r--spec/controllers/groups_controller_spec.rb64
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb18
-rw-r--r--spec/controllers/import/github_controller_spec.rb58
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb133
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb14
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb57
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb160
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb14
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb2
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb20
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb66
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb56
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/learn_gitlab_controller_spec.rb49
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb36
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb102
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb146
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb42
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb762
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb48
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb79
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb14
-rw-r--r--spec/controllers/projects/service_ping_controller_spec.rb77
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb34
-rw-r--r--spec/controllers/projects_controller_spec.rb59
-rw-r--r--spec/controllers/registrations_controller_spec.rb18
-rw-r--r--spec/controllers/root_controller_spec.rb22
-rw-r--r--spec/controllers/search_controller_spec.rb5
-rw-r--r--spec/controllers/sessions_controller_spec.rb2
-rw-r--r--spec/db/development/create_base_work_item_types_spec.rb2
-rw-r--r--spec/db/docs_spec.rb41
-rw-r--r--spec/db/migration_spec.rb2
-rw-r--r--spec/db/production/create_base_work_item_types_spec.rb2
-rw-r--r--spec/db/schema_spec.rb20
-rw-r--r--spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb9
-rw-r--r--spec/factories/abuse_reports.rb1
-rw-r--r--spec/factories/airflow/dags.rb8
-rw-r--r--spec/factories/analytics/cycle_analytics/project_stages.rb16
-rw-r--r--spec/factories/analytics/cycle_analytics/project_value_streams.rb9
-rw-r--r--spec/factories/analytics/cycle_analytics/stages.rb20
-rw-r--r--spec/factories/analytics/cycle_analytics/value_streams.rb9
-rw-r--r--spec/factories/bulk_import/entities.rb2
-rw-r--r--spec/factories/ci/bridge.rb8
-rw-r--r--spec/factories/ci/builds.rb1
-rw-r--r--spec/factories/ci/pipelines.rb4
-rw-r--r--spec/factories/ci/processable.rb26
-rw-r--r--spec/factories/ci/runner_machines.rb7
-rw-r--r--spec/factories/clusters/applications/helm.rb9
-rw-r--r--spec/factories/clusters/clusters.rb2
-rw-r--r--spec/factories/commit_statuses.rb1
-rw-r--r--spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb9
-rw-r--r--spec/factories/lfs_objects.rb7
-rw-r--r--spec/factories/member_roles.rb3
-rw-r--r--spec/factories/packages/debian/distribution.rb10
-rw-r--r--spec/factories/packages/debian/group_architecture.rb2
-rw-r--r--spec/factories/packages/debian/group_component.rb2
-rw-r--r--spec/factories/packages/debian/project_architecture.rb2
-rw-r--r--spec/factories/packages/debian/project_component.rb2
-rw-r--r--spec/factories/packages/packages.rb4
-rw-r--r--spec/factories/projects.rb31
-rw-r--r--spec/factories/projects/data_transfers.rb9
-rw-r--r--spec/factories/protected_tags/create_access_levels.rb9
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/factories/work_items/widget_definitions.rb11
-rw-r--r--spec/fast_spec_helper.rb2
-rw-r--r--spec/features/abuse_report_spec.rb6
-rw-r--r--spec/features/admin/admin_appearance_spec.rb23
-rw-r--r--spec/features/admin/admin_groups_spec.rb1
-rw-r--r--spec/features/admin/admin_hooks_spec.rb8
-rw-r--r--spec/features/admin/admin_jobs_spec.rb5
-rw-r--r--spec/features/admin/admin_projects_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb26
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb12
-rw-r--r--spec/features/admin/admin_settings_spec.rb107
-rw-r--r--spec/features/admin/users/users_spec.rb5
-rw-r--r--spec/features/boards/issue_ordering_spec.rb35
-rw-r--r--spec/features/boards/new_issue_spec.rb4
-rw-r--r--spec/features/boards/reload_boards_on_browser_back_spec.rb2
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/broadcast_messages_spec.rb6
-rw-r--r--spec/features/calendar_spec.rb9
-rw-r--r--spec/features/callouts/registration_enabled_spec.rb2
-rw-r--r--spec/features/commits_spec.rb3
-rw-r--r--spec/features/dashboard/activity_spec.rb12
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb8
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb4
-rw-r--r--spec/features/dashboard/issues_spec.rb17
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb9
-rw-r--r--spec/features/dashboard/milestones_spec.rb14
-rw-r--r--spec/features/dashboard/project_member_activity_index_spec.rb2
-rw-r--r--spec/features/dashboard/root_explore_spec.rb12
-rw-r--r--spec/features/explore/topics_spec.rb2
-rw-r--r--spec/features/explore/user_explores_projects_spec.rb2
-rw-r--r--spec/features/file_uploads/user_avatar_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/empty_states_spec.rb8
-rw-r--r--spec/features/groups/group_runners_spec.rb13
-rw-r--r--spec/features/groups/group_settings_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb8
-rw-r--r--spec/features/groups/settings/packages_and_registries_spec.rb2
-rw-r--r--spec/features/groups/show_spec.rb56
-rw-r--r--spec/features/ide/clientside_preview_csp_spec.rb31
-rw-r--r--spec/features/incidents/incident_details_spec.rb21
-rw-r--r--spec/features/incidents/incident_timeline_events_spec.rb123
-rw-r--r--spec/features/incidents/user_views_incident_spec.rb4
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb1
-rw-r--r--spec/features/issuables/sorting_list_spec.rb7
-rw-r--r--spec/features/issues/form_spec.rb4
-rw-r--r--spec/features/issues/incident_issue_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb4
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb27
-rw-r--r--spec/features/issues/move_spec.rb24
-rw-r--r--spec/features/issues/spam_akismet_issue_creation_spec.rb20
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb7
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb4
-rw-r--r--spec/features/jira_connect/branches_spec.rb20
-rw-r--r--spec/features/markdown/markdown_spec.rb5
-rw-r--r--spec/features/markdown/math_spec.rb77
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb10
-rw-r--r--spec/features/merge_request/user_can_see_draft_toggle_spec.rb47
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb44
-rw-r--r--spec/features/merge_request/user_creates_mr_spec.rb4
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb20
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_resolves_wip_mr_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb5
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb9
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb61
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb32
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb8
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb88
-rw-r--r--spec/features/nav/top_nav_spec.rb45
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/oauth_registration_spec.rb1
-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.rb77
-rw-r--r--spec/features/profiles/list_users_saved_replies_spec.rb21
-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.rb2
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb2
-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.rb3
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb2
-rw-r--r--spec/features/projects/artifacts/user_views_project_artifacts_page_spec.rb40
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb12
-rw-r--r--spec/features/projects/branches/new_branch_ref_dropdown_spec.rb23
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb8
-rw-r--r--spec/features/projects/commits/multi_view_diff_spec.rb4
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb29
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb41
-rw-r--r--spec/features/projects/graph_spec.rb21
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb1
-rw-r--r--spec/features/projects/integrations/user_activates_slack_notifications_spec.rb1
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_spec.rb22
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb4
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb58
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/features/projects/network_graph_spec.rb18
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb5
-rw-r--r--spec/features/projects/pipelines/legacy_pipelines_spec.rb 0
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb17
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb6
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb2
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb4
-rw-r--r--spec/features/projects/settings/packages_settings_spec.rb35
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_changes_default_branch_spec.rb8
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb3
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb4
-rw-r--r--spec/features/projects/show/clone_button_spec.rb43
-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/tree/tree_show_spec.rb27
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb6
-rw-r--r--spec/features/projects_spec.rb8
-rw-r--r--spec/features/runners_spec.rb32
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb318
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb56
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb72
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb166
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb88
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb62
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb107
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb71
-rw-r--r--spec/features/signed_commits_spec.rb26
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb21
-rw-r--r--spec/features/snippets_spec.rb2
-rw-r--r--spec/features/tags/developer_creates_tag_spec.rb6
-rw-r--r--spec/features/task_lists_spec.rb78
-rw-r--r--spec/features/triggers_spec.rb15
-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/users/add_email_to_existing_account_spec.rb2
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb1
-rw-r--r--spec/features/users/overview_spec.rb3
-rw-r--r--spec/features/users/rss_spec.rb2
-rw-r--r--spec/features/users/show_spec.rb65
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb6
-rw-r--r--spec/features/users/terms_spec.rb2
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb6
-rw-r--r--spec/features/users/zuora_csp_spec.rb20
-rw-r--r--spec/features/webauthn_spec.rb276
-rw-r--r--spec/features/work_items/work_item_children_spec.rb10
-rw-r--r--spec/features/work_items/work_item_spec.rb49
-rw-r--r--spec/finders/analytics/cycle_analytics/stage_finder_spec.rb2
-rw-r--r--spec/finders/ci/freeze_periods_finder_spec.rb2
-rw-r--r--spec/finders/ci/pipeline_schedules_finder_spec.rb43
-rw-r--r--spec/finders/ci/pipelines_finder_spec.rb4
-rw-r--r--spec/finders/ci/runners_finder_spec.rb6
-rw-r--r--spec/finders/concerns/finder_with_group_hierarchy_spec.rb98
-rw-r--r--spec/finders/fork_targets_finder_spec.rb14
-rw-r--r--spec/finders/group_members_finder_spec.rb50
-rw-r--r--spec/finders/groups_finder_spec.rb12
-rw-r--r--spec/finders/merge_request_target_project_finder_spec.rb24
-rw-r--r--spec/finders/merge_requests_finder_spec.rb24
-rw-r--r--spec/finders/namespaces/projects_finder_spec.rb34
-rw-r--r--spec/finders/projects/ml/candidate_finder_spec.rb79
-rw-r--r--spec/finders/projects_finder_spec.rb20
-rw-r--r--spec/finders/protected_branches_finder_spec.rb56
-rw-r--r--spec/finders/releases/group_releases_finder_spec.rb2
-rw-r--r--spec/finders/todos_finder_spec.rb4
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_degradation.json5
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json146
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json2
-rw-r--r--spec/fixtures/blockquote_fence_after.md29
-rw-r--r--spec/fixtures/blockquote_fence_before.md29
-rw-r--r--spec/fixtures/build_artifacts/dotenv_utf16_le.txtbin0 -> 62 bytes
-rw-r--r--spec/fixtures/database.sql.gzbin0 -> 30 bytes
-rw-r--r--spec/fixtures/emails/html_only.eml45
-rw-r--r--spec/fixtures/emails/html_table_and_blockquote.eml41
-rw-r--r--spec/fixtures/lib/gitlab/email/basic.html72
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json682
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json6
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json4
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json4
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json6
-rw-r--r--spec/fixtures/mail_room/encrypted_secrets/incoming_email.yaml.enc1
-rw-r--r--spec/fixtures/mail_room/encrypted_secrets/service_desk_email.yaml.enc1
-rw-r--r--spec/fixtures/mail_room/secrets.yml.erb11
-rw-r--r--spec/fixtures/markdown.md.erb4
-rw-r--r--spec/fixtures/structure.sql20
-rw-r--r--spec/fixtures/svg_without_attr.svg17
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js3
-rw-r--r--spec/frontend/__helpers__/test_apollo_link.js4
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js7
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js8
-rw-r--r--spec/frontend/abuse_reports/components/links_to_spam_input_spec.js65
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js7
-rw-r--r--spec/frontend/admin/broadcast_messages/components/base_spec.js7
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js3
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js11
-rw-r--r--spec/frontend/admin/statistics_panel/store/mutations_spec.js6
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js122
-rw-r--r--spec/frontend/airflow/dags/components/dags_spec.js115
-rw-r--r--spec/frontend/airflow/dags/components/mock_data.js67
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap1
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js66
-rw-r--r--spec/frontend/api/alert_management_alerts_api_spec.js13
-rw-r--r--spec/frontend/api/groups_api_spec.js32
-rw-r--r--spec/frontend/api/projects_api_spec.js83
-rw-r--r--spec/frontend/api/user_api_spec.js9
-rw-r--r--spec/frontend/api_spec.js22
-rw-r--r--spec/frontend/artifacts/components/app_spec.js109
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js3
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js3
-rw-r--r--spec/frontend/badges/store/actions_spec.js21
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js58
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js29
-rw-r--r--spec/frontend/batch_comments/components/publish_button_spec.js34
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js17
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js23
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js48
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js7
-rw-r--r--spec/frontend/blob/openapi/index_spec.js6
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js15
-rw-r--r--spec/frontend/boards/board_list_helper.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js33
-rw-r--r--spec/frontend/boards/components/board_app_spec.js2
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js52
-rw-r--r--spec/frontend/boards/components/board_card_spec.js1
-rw-r--r--spec/frontend/boards/components/board_column_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js15
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js34
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js52
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js33
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js23
-rw-r--r--spec/frontend/boards/stores/actions_spec.js18
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js4
-rw-r--r--spec/frontend/branches/components/sort_dropdown_spec.js8
-rw-r--r--spec/frontend/branches/divergence_graph_spec.js3
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js37
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js109
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js45
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js62
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js107
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js98
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js1
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js28
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/actions_spec.js19
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js80
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js108
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js15
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js32
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js127
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js87
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js19
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js71
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js96
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js154
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js10
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js40
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js14
-rw-r--r--spec/frontend/ci/runner/mock_data.js1
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap386
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/button_spec.js49
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/modal_spec.js78
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js17
-rw-r--r--spec/frontend/ci_secure_files/mock_data.js61
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js94
-rw-r--r--spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js2
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js9
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js7
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js9
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js5
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js19
-rw-r--r--spec/frontend/commit/mock_data.js153
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js17
-rw-r--r--spec/frontend/commits_spec.js3
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js100
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js3
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js15
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap46
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js66
-rw-r--r--spec/frontend/contributors/store/actions_spec.js5
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js9
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js5
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js7
-rw-r--r--spec/frontend/design_management/components/__snapshots__/image_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js305
-rw-r--r--spec/frontend/design_management/components/image_spec.js10
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap12
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js6
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap243
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js30
-rw-r--r--spec/frontend/diffs/components/app_spec.js3
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js525
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js36
-rw-r--r--spec/frontend/dropzone_input_spec.js4
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js11
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js17
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js4
-rw-r--r--spec/frontend/environments/edit_environment_spec.js8
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_actions_spec.js47
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_job_spec.js (renamed from spec/frontend/environments/environment_details/deployment_job_spec.js)0
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_status_link_spec.js (renamed from spec/frontend/environments/environment_details/deployment_status_link_spec.js)0
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js (renamed from spec/frontend/environments/environment_details/deployment_triggerer_spec.js)0
-rw-r--r--spec/frontend/environments/environment_form_spec.js9
-rw-r--r--spec/frontend/environments/environments_app_spec.js30
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js3
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js50
-rw-r--r--spec/frontend/environments/graphql/mock_data.js8
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js25
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap10
-rw-r--r--spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js31
-rw-r--r--spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js69
-rw-r--r--spec/frontend/environments/new_environment_spec.js12
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js60
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js5
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js11
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js9
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js33
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js15
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js17
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js7
-rw-r--r--spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js3
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js3
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js2
-rw-r--r--spec/frontend/fixtures/jobs.rb1
-rw-r--r--spec/frontend/fixtures/listbox.rb5
-rw-r--r--spec/frontend/fixtures/merge_requests.rb47
-rw-r--r--spec/frontend/fixtures/pipelines.rb11
-rw-r--r--spec/frontend/fixtures/runner.rb2
-rw-r--r--spec/frontend/fixtures/saved_replies.rb46
-rw-r--r--spec/frontend/fixtures/static/project_select_combo_button.html13
-rw-r--r--spec/frontend/flash_spec.js108
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js6
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js3
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js1
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js8
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js18
-rw-r--r--spec/frontend/gl_form_spec.js41
-rw-r--r--spec/frontend/gpg_badges_spec.js19
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js26
-rw-r--r--spec/frontend/groups/components/app_spec.js24
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js5
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js21
-rw-r--r--spec/frontend/header_search/components/app_spec.js4
-rw-r--r--spec/frontend/header_search/store/actions_spec.js7
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js61
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js25
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js416
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js161
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js3
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js13
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js32
-rw-r--r--spec/frontend/ide/lib/mirror_spec.js10
-rw-r--r--spec/frontend/ide/remote/index_spec.js3
-rw-r--r--spec/frontend/ide/services/index_spec.js9
-rw-r--r--spec/frontend/ide/services/terminals_spec.js3
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js9
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js13
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js5
-rw-r--r--spec/frontend/ide/stores/actions_spec.js5
-rw-r--r--spec/frontend/ide/stores/getters_spec.js12
-rw-r--r--spec/frontend/ide/stores/modules/branches/actions_spec.js9
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js38
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js3
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/actions_spec.js13
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/actions_spec.js9
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js25
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js19
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js19
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js5
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js78
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js3
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js67
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js43
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js26
-rw-r--r--spec/frontend/import_entities/import_projects/utils_spec.js2
-rw-r--r--spec/frontend/integrations/index/components/integrations_table_spec.js57
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js107
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js15
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js56
-rw-r--r--spec/frontend/invite_members/mock_data/api_response_data.js2
-rw-r--r--spec/frontend/issuable/helpers.js18
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js72
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js15
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js6
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js17
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js76
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js14
-rw-r--r--spec/frontend/issues/dashboard/utils_spec.js5
-rw-r--r--spec/frontend/issues/issue_spec.js3
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js10
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js6
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js95
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js133
-rw-r--r--spec/frontend/issues/list/mock_data.js51
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js3
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js7
-rw-r--r--spec/frontend/issues/show/components/app_spec.js40
-rw-r--r--spec/frontend/issues/show/components/description_spec.js237
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js62
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js20
-rw-r--r--spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js6
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js37
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js49
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js39
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js33
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js4
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js48
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js18
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js61
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js54
-rw-r--r--spec/frontend/issues/show/issue_spec.js7
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js8
-rw-r--r--spec/frontend/issues/show/utils_spec.js272
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js74
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js49
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js32
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js22
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js19
-rw-r--r--spec/frontend/jobs/store/actions_spec.js22
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js9
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js2
-rw-r--r--spec/frontend/lazy_loader_spec.js6
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json3089
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json3091
-rw-r--r--spec/frontend/lib/apollo/mock_data/non_persisted_cache.json3089
-rw-r--r--spec/frontend/lib/apollo/persist_link_spec.js74
-rw-r--r--spec/frontend/lib/apollo/persistence_mapper_spec.js163
-rw-r--r--spec/frontend/lib/utils/ajax_cache_spec.js7
-rw-r--r--spec/frontend/lib/utils/apollo_startup_js_link_spec.js11
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js17
-rw-r--r--spec/frontend/lib/utils/axios_utils_spec.js9
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js111
-rw-r--r--spec/frontend/lib/utils/favicon_ci_spec.js5
-rw-r--r--spec/frontend/lib/utils/icon_utils_spec.js9
-rw-r--r--spec/frontend/lib/utils/poll_spec.js22
-rw-r--r--spec/frontend/lib/utils/rails_ujs_spec.js3
-rw-r--r--spec/frontend/lib/utils/scroll_utils_spec.js21
-rw-r--r--spec/frontend/lib/utils/select2_utils_spec.js100
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js36
-rw-r--r--spec/frontend/listbox/index_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js6
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js9
-rw-r--r--spec/frontend/members/utils_spec.js6
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js11
-rw-r--r--spec/frontend/merge_request_spec.js5
-rw-r--r--spec/frontend/merge_requests/components/compare_app_spec.js50
-rw-r--r--spec/frontend/merge_requests/components/compare_dropdown_spec.js (renamed from spec/frontend/merge_requests/components/target_project_dropdown_spec.js)40
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js7
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js37
-rw-r--r--spec/frontend/milestones/components/promote_milestone_modal_spec.js3
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap8
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap761
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js2
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js359
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js110
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js21
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap3
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap7
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js7
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js31
-rw-r--r--spec/frontend/mr_notes/stores/actions_spec.js6
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js5
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js1
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js1
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js23
-rw-r--r--spec/frontend/notes/components/attachments_warning_spec.js16
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js64
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js16
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js3
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js54
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js3
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js7
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js3
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js61
-rw-r--r--spec/frontend/notes/mock_data.js5
-rw-r--r--spec/frontend/notes/stores/actions_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap1
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap104
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js90
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js109
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js)78
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap25
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js43
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js44
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js64
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_list_spec.js28
-rw-r--r--spec/frontend/pager_spec.js3
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js67
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js)21
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js57
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js155
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js3
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js9
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js5
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js16
-rw-r--r--spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js39
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js38
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap109
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js4
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap62
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap409
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js27
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js233
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js113
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js12
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/mock_data.js73
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js237
-rw-r--r--spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap7
-rw-r--r--spec/frontend/pages/search/show/refresh_counts_spec.js43
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js11
-rw-r--r--spec/frontend/performance_bar/index_spec.js3
-rw-r--r--spec/frontend/persistent_user_callout_spec.js13
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js17
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js95
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js49
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js19
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js308
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js44
-rw-r--r--spec/frontend/pipelines/linked_pipelines_mock.json127
-rw-r--r--spec/frontend/pipelines/mock_data.js2
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js9
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js67
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js8
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js5
-rw-r--r--spec/frontend/pipelines/utils_spec.js37
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js12
-rw-r--r--spec/frontend/profile/components/activity_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/contributed_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/groups_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/overview_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/personal_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js36
-rw-r--r--spec/frontend/profile/components/snippets_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/starred_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap8
-rw-r--r--spec/frontend/project_select_combo_button_spec.js165
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js133
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js64
-rw-r--r--spec/frontend/projects/commit/mock_data.js6
-rw-r--r--spec/frontend/projects/commit/store/getters_spec.js8
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js7
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js7
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js7
-rw-r--r--spec/frontend/projects/project_find_file_spec.js3
-rw-r--r--spec/frontend/projects/project_new_spec.js18
-rw-r--r--spec/frontend/projects/prune_unreachable_objects_button_spec.js72
-rw-r--r--spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js (renamed from spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js)4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js73
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js42
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js1
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js8
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js9
-rw-r--r--spec/frontend/projects/settings/mock_data.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js56
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js3
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js3
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js7
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js246
-rw-r--r--spec/frontend/ref/format_refs_spec.js38
-rw-r--r--spec/frontend/ref/mock_data.js87
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js5
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js16
-rw-r--r--spec/frontend/releases/components/app_index_spec.js14
-rw-r--r--spec/frontend/releases/components/app_show_spec.js7
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js8
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js36
-rw-r--r--spec/frontend/releases/components/releases_empty_state_spec.js39
-rw-r--r--spec/frontend/releases/release_notification_service_spec.js57
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js5
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js20
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js2
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js70
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js8
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js24
-rw-r--r--spec/frontend/repository/log_tree_spec.js3
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js106
-rw-r--r--spec/frontend/repository/mock_data.js4
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js6
-rw-r--r--spec/frontend/right_sidebar_spec.js3
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap21
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js22
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js68
-rw-r--r--spec/frontend/search/mock_data.js461
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js63
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js85
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js20
-rw-r--r--spec/frontend/search/sidebar/components/language_filters_spec.js152
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js20
-rw-r--r--spec/frontend/search/sidebar/utils_spec.js10
-rw-r--r--spec/frontend/search/store/actions_spec.js58
-rw-r--r--spec/frontend/search/store/getters_spec.js20
-rw-r--r--spec/frontend/search/store/mutations_spec.js14
-rw-r--r--spec/frontend/search_autocomplete_spec.js3
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js10
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js8
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js9
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js16
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js9
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/move_issue_button_spec.js157
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js7
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js40
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js6
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js6
-rw-r--r--spec/frontend/sidebar/lib/sidebar_move_issue_spec.js162
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js11
-rw-r--r--spec/frontend/single_file_diff_spec.js7
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js7
-rw-r--r--spec/frontend/super_sidebar/components/counter_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js39
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js152
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js46
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js10
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js21
-rw-r--r--spec/frontend/super_sidebar/mock_data.js70
-rw-r--r--spec/frontend/terms/components/app_spec.js7
-rw-r--r--spec/frontend/token_access/inbound_token_access_spec.js311
-rw-r--r--spec/frontend/token_access/mock_data.js122
-rw-r--r--spec/frontend/token_access/opt_in_jwt_spec.js144
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js (renamed from spec/frontend/token_access/token_access_spec.js)8
-rw-r--r--spec/frontend/token_access/token_access_app_spec.js47
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js7
-rw-r--r--spec/frontend/tracking/get_standard_context_spec.js2
-rw-r--r--spec/frontend/usage_quotas/components/usage_quotas_app_spec.js39
-rw-r--r--spec/frontend/usage_quotas/mock_data.js3
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js2
-rw-r--r--spec/frontend/users_select/test_helper.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js44
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js33
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js23
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js18
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js14
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js576
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js9
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js42
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js182
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js43
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js15
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js90
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js15
-rw-r--r--spec/frontend/vue_merge_request_widget/test_extensions.js5
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js268
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js248
-rw-r--r--spec/frontend/vue_shared/components/entity_select/utils_spec.js (renamed from spec/frontend/vue_shared/components/group_select/utils_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js322
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js)17
-rw-r--r--spec/frontend/vue_shared/components/incubation/pagination_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js54
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js262
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js123
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js94
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js177
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js156
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js13
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js17
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js9
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/security_reports/store/utils_spec.js63
-rw-r--r--spec/frontend/webhooks/components/test_dropdown_spec.js63
-rw-r--r--spec/frontend/whats_new/components/app_spec.js1
-rw-r--r--spec/frontend/whats_new/store/actions_spec.js7
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap3
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_form_spec.js)123
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js164
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_locked_spec.js)2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js149
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js52
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_replying_spec.js34
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js256
-rw-r--r--spec/frontend/work_items/components/widget_wrapper_spec.js46
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js104
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js44
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js171
-rw-r--r--spec/frontend/work_items/mock_data.js443
-rw-r--r--spec/frontend/work_items/utils_spec.js27
-rw-r--r--spec/frontend/zen_mode_spec.js3
-rw-r--r--spec/frontend_integration/ide/helpers/mock_data.js2
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/404.js3
-rw-r--r--spec/frontend_integration/test_helpers/mock_server/routes/emojis.js3
-rw-r--r--spec/graphql/mutations/achievements/create_spec.rb2
-rw-r--r--spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb40
-rw-r--r--spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb37
-rw-r--r--spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb9
-rw-r--r--spec/graphql/mutations/issues/update_spec.rb41
-rw-r--r--spec/graphql/mutations/merge_requests/update_spec.rb103
-rw-r--r--spec/graphql/mutations/saved_replies/create_spec.rb2
-rw-r--r--spec/graphql/mutations/saved_replies/update_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/variables_resolver_spec.rb70
-rw-r--r--spec/graphql/resolvers/data_transfer_resolver_spec.rb31
-rw-r--r--spec/graphql/resolvers/group_releases_resolver_spec.rb47
-rw-r--r--spec/graphql/resolvers/groups_resolver_spec.rb127
-rw-r--r--spec/graphql/resolvers/nested_groups_resolver_spec.rb133
-rw-r--r--spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/releases_resolver_spec.rb64
-rw-r--r--spec/graphql/resolvers/saved_reply_resolver_spec.rb40
-rw-r--r--spec/graphql/resolvers/users/participants_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/work_items_resolver_spec.rb4
-rw-r--r--spec/graphql/types/achievements/achievement_type_spec.rb3
-rw-r--r--spec/graphql/types/ci/job_token_scope_type_spec.rb84
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/runner_architecture_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/runner_platform_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/runner_setup_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb3
-rw-r--r--spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb3
-rw-r--r--spec/graphql/types/ci/runner_web_url_edge_spec.rb2
-rw-r--r--spec/graphql/types/ci/variable_sort_enum_spec.rb12
-rw-r--r--spec/graphql/types/commit_signatures/verification_status_enum_spec.rb2
-rw-r--r--spec/graphql/types/group_type_spec.rb9
-rw-r--r--spec/graphql/types/issue_type_spec.rb26
-rw-r--r--spec/graphql/types/notes/deleted_note_type_spec.rb15
-rw-r--r--spec/graphql/types/packages/package_details_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/merge_request_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/work_item_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb64
-rw-r--r--spec/graphql/types/query_type_spec.rb42
-rw-r--r--spec/graphql/types/user_type_spec.rb3
-rw-r--r--spec/graphql/types/users/email_type_spec.rb2
-rw-r--r--spec/graphql/types/users/namespace_commit_email_type_spec.rb2
-rw-r--r--spec/graphql/types/work_item_type_spec.rb1
-rw-r--r--spec/haml_lint/linter/documentation_links_spec.rb13
-rw-r--r--spec/helpers/admin/user_actions_helper_spec.rb18
-rw-r--r--spec/helpers/appearances_helper_spec.rb79
-rw-r--r--spec/helpers/application_helper_spec.rb45
-rw-r--r--spec/helpers/application_settings_helper_spec.rb61
-rw-r--r--spec/helpers/artifacts_helper_spec.rb36
-rw-r--r--spec/helpers/bizible_helper_spec.rb42
-rw-r--r--spec/helpers/ci/variables_helper_spec.rb11
-rw-r--r--spec/helpers/emails_helper_spec.rb10
-rw-r--r--spec/helpers/form_helper_spec.rb83
-rw-r--r--spec/helpers/hooks_helper_spec.rb27
-rw-r--r--spec/helpers/ide_helper_spec.rb39
-rw-r--r--spec/helpers/invite_members_helper_spec.rb52
-rw-r--r--spec/helpers/issuables_helper_spec.rb69
-rw-r--r--spec/helpers/issues_helper_spec.rb21
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb42
-rw-r--r--spec/helpers/learn_gitlab_helper_spec.rb162
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb2
-rw-r--r--spec/helpers/namespaces_helper_spec.rb112
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb189
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb4
-rw-r--r--spec/helpers/page_layout_helper_spec.rb9
-rw-r--r--spec/helpers/preferences_helper_spec.rb1
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb68
-rw-r--r--spec/helpers/projects_helper_spec.rb82
-rw-r--r--spec/helpers/registrations_helper_spec.rb16
-rw-r--r--spec/helpers/sidebars_helper_spec.rb114
-rw-r--r--spec/helpers/snippets_helper_spec.rb15
-rw-r--r--spec/helpers/timeboxes_routing_helper_spec.rb48
-rw-r--r--spec/helpers/users/callouts_helper_spec.rb9
-rw-r--r--spec/helpers/web_hooks/web_hooks_helper_spec.rb18
-rw-r--r--spec/initializers/00_deprecations_spec.rb172
-rw-r--r--spec/initializers/0_log_deprecations_spec.rb138
-rw-r--r--spec/initializers/0_postgresql_types_spec.rb2
-rw-r--r--spec/initializers/check_forced_decomposition_spec.rb124
-rw-r--r--spec/initializers/countries_spec.rb15
-rw-r--r--spec/initializers/database_config_spec.rb2
-rw-r--r--spec/initializers/google_api_client_spec.rb3
-rw-r--r--spec/initializers/load_balancing_spec.rb2
-rw-r--r--spec/initializers/memory_watchdog_spec.rb8
-rw-r--r--spec/lib/api/ci/helpers/runner_helpers_spec.rb7
-rw-r--r--spec/lib/api/ci/helpers/runner_spec.rb78
-rw-r--r--spec/lib/api/entities/draft_note_spec.rb18
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb28
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_info_spec.rb2
-rw-r--r--spec/lib/api/entities/release_spec.rb12
-rw-r--r--spec/lib/api/entities/user_spec.rb2
-rw-r--r--spec/lib/api/helpers/caching_spec.rb2
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb56
-rw-r--r--spec/lib/api/helpers_spec.rb2
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb2
-rw-r--r--spec/lib/backup/database_spec.rb219
-rw-r--r--spec/lib/backup/dump/postgres_spec.rb36
-rw-r--r--spec/lib/backup/manager_spec.rb15
-rw-r--r--spec/lib/banzai/color_parser_spec.rb2
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb2
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb2
-rw-r--r--spec/lib/banzai/filter/absolute_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/attributes_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/audio_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/blockquote_fence_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/color_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/custom_emoji_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/footnote_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/front_matter_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/html_entity_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/image_lazy_load_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/kroki_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/mermaid_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/normalize_source_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/output_safety_spec.rb2
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/reference_redactor_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/alert_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/commit_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/design_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb52
-rw-r--r--spec/lib/banzai/filter/references/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/project_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/spaced_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/suggestion_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/truncate_source_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/truncate_visible_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb40
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter_array_spec.rb2
-rw-r--r--spec/lib/banzai/issuable_extractor_spec.rb2
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/description_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/email_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/emoji_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb36
-rw-r--r--spec/lib/banzai/pipeline/gfm_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb13
-rw-r--r--spec/lib/banzai/pipeline/post_process_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline_spec.rb2
-rw-r--r--spec/lib/banzai/querying_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/alert_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/design_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/project_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_redactor_spec.rb2
-rw-r--r--spec/lib/banzai/render_context_spec.rb2
-rw-r--r--spec/lib/banzai/renderer_spec.rb2
-rw-r--r--spec/lib/bulk_imports/clients/graphql_spec.rb58
-rw-r--r--spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb6
-rw-r--r--spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb6
-rw-r--r--spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb14
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb12
-rw-r--r--spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb56
-rw-r--r--spec/lib/bulk_imports/pipeline/context_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb51
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb4
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb31
-rw-r--r--spec/lib/extracts_ref_spec.rb12
-rw-r--r--spec/lib/feature_groups/gitlab_team_members_spec.rb65
-rw-r--r--spec/lib/feature_spec.rb29
-rw-r--r--spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb127
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb2
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb2
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/base_query_builder_spec.rb12
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb4
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb2
-rw-r--r--spec/lib/gitlab/api_authentication/token_resolver_spec.rb6
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb18
-rw-r--r--spec/lib/gitlab/auth/ip_rate_limiter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb61
-rw-r--r--spec/lib/gitlab/auth_spec.rb45
-rw-r--r--spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb65
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb11
-rw-r--r--spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb57
-rw-r--r--spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb242
-rw-r--r--spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb90
-rw-r--r--spec/lib/gitlab/background_migration/rebalance_partition_id_spec.rb46
-rw-r--r--spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb102
-rw-r--r--spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/helpers_spec.rb27
-rw-r--r--spec/lib/gitlab/cache/metadata_spec.rb94
-rw-r--r--spec/lib/gitlab/cache/metrics_spec.rb24
-rw-r--r--spec/lib/gitlab/chat/responder_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/artifacts/logger_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb116
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/external/context_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/external/file/artifact_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb179
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb78
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb91
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/yaml_spec.rb105
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/interpolation/access_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/interpolation/block_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/interpolation/config_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/interpolation/context_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/interpolation/template_spec.rb102
-rw-r--r--spec/lib/gitlab/ci/parsers/instrumentation_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb202
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb72
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb1615
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb115
-rw-r--r--spec/lib/gitlab/ci/runner_instructions_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/runner_releases_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/runner_upgrade_check_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/status/bridge/common_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/trace/archive_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb336
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb2
-rw-r--r--spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb99
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb20
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb2
-rw-r--r--spec/lib/gitlab/database/async_ddl_exclusive_lease_guard_spec.rb (renamed from spec/lib/gitlab/database/indexing_exclusive_lease_guard_spec.rb)18
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb152
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb167
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb52
-rw-r--r--spec/lib/gitlab/database/async_foreign_keys_spec.rb23
-rw-r--r--spec/lib/gitlab/database/async_indexes/index_base_spec.rb81
-rw-r--r--spec/lib/gitlab/database/async_indexes/index_creator_spec.rb40
-rw-r--r--spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb40
-rw-r--r--spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb36
-rw-r--r--spec/lib/gitlab/database/async_indexes_spec.rb57
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb2
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb10
-rw-r--r--spec/lib/gitlab/database/load_balancing/sticking_spec.rb443
-rw-r--r--spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb52
-rw-r--r--spec/lib/gitlab/database/load_balancing_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb10
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb92
-rw-r--r--spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migrations/instrumentation_spec.rb11
-rw-r--r--spec/lib/gitlab/database/migrations/observers/batch_details_spec.rb42
-rw-r--r--spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb52
-rw-r--r--spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/partitioning/partition_manager_spec.rb58
-rw-r--r--spec/lib/gitlab/database/partitioning_spec.rb4
-rw-r--r--spec/lib/gitlab/database/postgres_foreign_key_spec.rb106
-rw-r--r--spec/lib/gitlab/database/postgres_index_spec.rb4
-rw-r--r--spec/lib/gitlab/database/postgres_partitioned_table_spec.rb62
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb12
-rw-r--r--spec/lib/gitlab/database/reflection_spec.rb2
-rw-r--r--spec/lib/gitlab/database/reindexing/coordinator_spec.rb2
-rw-r--r--spec/lib/gitlab/database/reindexing_spec.rb19
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb3
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb3
-rw-r--r--spec/lib/gitlab/database/schema_migrations/context_spec.rb6
-rw-r--r--spec/lib/gitlab/database/schema_validation/database_spec.rb45
-rw-r--r--spec/lib/gitlab/database/schema_validation/index_spec.rb22
-rw-r--r--spec/lib/gitlab/database/schema_validation/indexes_spec.rb56
-rw-r--r--spec/lib/gitlab/database/shared_model_spec.rb55
-rw-r--r--spec/lib/gitlab/database/tables_locker_spec.rb226
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb6
-rw-r--r--spec/lib/gitlab/database/transaction/observer_spec.rb2
-rw-r--r--spec/lib/gitlab/database/transaction_timeout_settings_spec.rb37
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb2
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb2
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb2
-rw-r--r--spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb66
-rw-r--r--spec/lib/gitlab/deploy_key_access_spec.rb26
-rw-r--r--spec/lib/gitlab/email/html_to_markdown_parser_spec.rb46
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb76
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb8
-rw-r--r--spec/lib/gitlab/error_tracking_spec.rb24
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb18
-rw-r--r--spec/lib/gitlab/etag_caching/router/graphql_spec.rb6
-rw-r--r--spec/lib/gitlab/etag_caching/router/rails_spec.rb10
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/config_spec.rb23
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb130
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb89
-rw-r--r--spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb41
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb95
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb27
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb39
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb72
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/clients/proxy_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb16
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb20
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb13
-rw-r--r--spec/lib/gitlab/github_import/object_counter_spec.rb55
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb69
-rw-r--r--spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb (renamed from spec/lib/gitlab/graphql/deprecation_spec.rb)2
-rw-r--r--spec/lib/gitlab/graphql/markdown_field_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb24
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_export_equivalence_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb148
-rw-r--r--spec/lib/gitlab/import_export/project/relation_factory_spec.rb76
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/references_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/incident_management/pager_duty/incident_issue_description_spec.rb10
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb45
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb10
-rw-r--r--spec/lib/gitlab/logger_spec.rb24
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb121
-rw-r--r--spec/lib/gitlab/memory/watchdog/configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog/configurator_spec.rb11
-rw-r--r--spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb5
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb13
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb27
-rw-r--r--spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb119
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb86
-rw-r--r--spec/lib/gitlab/metrics/environment_spec.rb32
-rw-r--r--spec/lib/gitlab/metrics/global_search_slis_spec.rb65
-rw-r--r--spec/lib/gitlab/metrics/rails_slis_spec.rb90
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb52
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/subscribers/ldap_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_item_spec.rb2
-rw-r--r--spec/lib/gitlab/octokit/middleware_spec.rb33
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb52
-rw-r--r--spec/lib/gitlab/other_markup_spec.rb4
-rw-r--r--spec/lib/gitlab/pages/cache_control_spec.rb26
-rw-r--r--spec/lib/gitlab/pagination/offset_pagination_spec.rb4
-rw-r--r--spec/lib/gitlab/quick_actions/command_definition_spec.rb29
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb17
-rw-r--r--spec/lib/gitlab/redis/cluster_rate_limiting_spec.rb7
-rw-r--r--spec/lib/gitlab/redis/db_load_balancing_spec.rb52
-rw-r--r--spec/lib/gitlab/redis/duplicate_jobs_spec.rb84
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb59
-rw-r--r--spec/lib/gitlab/redis/rate_limiting_spec.rb17
-rw-r--r--spec/lib/gitlab/redis/repository_cache_spec.rb49
-rw-r--r--spec/lib/gitlab/redis/sidekiq_status_spec.rb5
-rw-r--r--spec/lib/gitlab/regex_spec.rb167
-rw-r--r--spec/lib/gitlab/repository_cache/preloader_spec.rb89
-rw-r--r--spec/lib/gitlab/repository_hash_cache_spec.rb31
-rw-r--r--spec/lib/gitlab/search/found_blob_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_death_handler_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb41
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb4
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb2
-rw-r--r--spec/lib/gitlab/slug/path_spec.rb33
-rw-r--r--spec/lib/gitlab/time_tracking_formatter_spec.rb22
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb11
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_ml_candidates_metric_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_ml_experiments_metric_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_candidates_metric_spec.rb18
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_experiments_metric_spec.rb15
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_monitor_enabled_metric_spec.rb22
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_with_ml_candidates_metric_spec.rb14
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb10
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/jira_active_integrations_metric_spec.rb39
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb10
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb8
-rw-r--r--spec/lib/gitlab/usage/service_ping_report_spec.rb21
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb16
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb48
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb10
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb30
-rw-r--r--spec/lib/gitlab/usage_data_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb22
-rw-r--r--spec/lib/gitlab/utils/email_spec.rb42
-rw-r--r--spec/lib/gitlab/utils_spec.rb20
-rw-r--r--spec/lib/initializer_connections_spec.rb36
-rw-r--r--spec/lib/marginalia_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb2
-rw-r--r--spec/lib/peek/views/active_record_spec.rb2
-rw-r--r--spec/lib/release_highlights/validator/entry_spec.rb56
-rw-r--r--spec/lib/release_highlights/validator_spec.rb2
-rw-r--r--spec/lib/service_ping/build_payload_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb79
-rw-r--r--spec/lib/sidebars/projects/menus/repository_menu_spec.rb15
-rw-r--r--spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb47
-rw-r--r--spec/lib/slack_markdown_sanitizer_spec.rb23
-rw-r--r--spec/mailers/emails/profile_spec.rb6
-rw-r--r--spec/mailers/emails/service_desk_spec.rb89
-rw-r--r--spec/mailers/notify_spec.rb92
-rw-r--r--spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb24
-rw-r--r--spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb45
-rw-r--r--spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb22
-rw-r--r--spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb76
-rw-r--r--spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb67
-rw-r--r--spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb31
-rw-r--r--spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb66
-rw-r--r--spec/migrations/20210804150320_create_base_work_item_types_spec.rb43
-rw-r--r--spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb137
-rw-r--r--spec/migrations/20210811122206_update_external_project_bots_spec.rb25
-rw-r--r--spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb62
-rw-r--r--spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb21
-rw-r--r--spec/migrations/20210914095310_cleanup_orphan_project_access_tokens_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/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb2
-rw-r--r--spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb2
-rw-r--r--spec/migrations/20220802204737_remove_deactivated_user_highest_role_stats_spec.rb2
-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.rb2
-rw-r--r--spec/migrations/20221013154159_update_invalid_dormant_user_setting_spec.rb2
-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/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb2
-rw-r--r--spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb2
-rw-r--r--spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb2
-rw-r--r--spec/migrations/20221209110935_fix_update_import_sources_on_application_settings_spec.rb2
-rw-r--r--spec/migrations/20221219122320_copy_clickhouse_connection_string_to_encrypted_var_spec.rb19
-rw-r--r--spec/migrations/20221226153252_queue_fix_incoherent_packages_size_on_project_statistics_spec.rb54
-rw-r--r--spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb23
-rw-r--r--spec/migrations/20230125093723_rebalance_partition_id_ci_pipeline_spec.rb58
-rw-r--r--spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb58
-rw-r--r--spec/migrations/20230130073109_nullify_creator_id_of_orphaned_projects_spec.rb32
-rw-r--r--spec/migrations/20230131125844_add_project_id_name_id_version_index_to_installable_npm_packages_spec.rb20
-rw-r--r--spec/migrations/20230201171450_finalize_backfill_environment_tier_migration_spec.rb72
-rw-r--r--spec/migrations/20230202131928_encrypt_ci_trigger_token_spec.rb84
-rw-r--r--spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb31
-rw-r--r--spec/migrations/20230208100917_fix_partition_ids_for_ci_pipeline_variable_spec.rb58
-rw-r--r--spec/migrations/20230208103009_fix_partition_ids_for_ci_job_artifact_spec.rb58
-rw-r--r--spec/migrations/20230208132608_fix_partition_ids_for_ci_stage_spec.rb58
-rw-r--r--spec/migrations/20230209090702_fix_partition_ids_for_ci_build_report_result_spec.rb60
-rw-r--r--spec/migrations/20230209092204_fix_partition_ids_for_ci_build_trace_metadata_spec.rb60
-rw-r--r--spec/migrations/20230209140102_fix_partition_ids_for_ci_build_metadata_spec.rb60
-rw-r--r--spec/migrations/20230214122717_fix_partition_ids_for_ci_job_variables_spec.rb51
-rw-r--r--spec/migrations/20230214154101_fix_partition_ids_on_ci_sources_pipelines_spec.rb45
-rw-r--r--spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb35
-rw-r--r--spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb69
-rw-r--r--spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb88
-rw-r--r--spec/migrations/add_projects_emails_enabled_column_data_spec.rb75
-rw-r--r--spec/migrations/add_triggers_to_integrations_type_new_spec.rb77
-rw-r--r--spec/migrations/add_upvotes_count_index_to_issues_spec.rb22
-rw-r--r--spec/migrations/associate_existing_dast_builds_with_variables_spec.rb10
-rw-r--r--spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb108
-rw-r--r--spec/migrations/backfill_integrations_type_new_spec.rb38
-rw-r--r--spec/migrations/backfill_issues_upvotes_count_spec.rb35
-rw-r--r--spec/migrations/backfill_stage_event_hash_spec.rb103
-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_remaining_orphan_invites_spec.rb37
-rw-r--r--spec/migrations/confirm_security_bot_spec.rb38
-rw-r--r--spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb46
-rw-r--r--spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb63
-rw-r--r--spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb42
-rw-r--r--spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb34
-rw-r--r--spec/migrations/orphaned_invite_tokens_cleanup_spec.rb50
-rw-r--r--spec/migrations/queue_backfill_user_details_fields_spec.rb2
-rw-r--r--spec/migrations/queue_populate_projects_star_count_spec.rb2
-rw-r--r--spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb62
-rw-r--r--spec/migrations/recount_epic_cache_counts_v3_spec.rb (renamed from spec/migrations/sanitize_confidential_note_todos_spec.rb)5
-rw-r--r--spec/migrations/remove_duplicate_dast_site_tokens_spec.rb53
-rw-r--r--spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb53
-rw-r--r--spec/migrations/remove_invalid_deploy_access_level_spec.rb48
-rw-r--r--spec/migrations/rename_services_to_integrations_spec.rb255
-rw-r--r--spec/migrations/replace_external_wiki_triggers_spec.rb132
-rw-r--r--spec/migrations/reschedule_delete_orphaned_deployments_spec.rb74
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_again_spec.rb25
-rw-r--r--spec/migrations/reset_job_token_scope_enabled_spec.rb25
-rw-r--r--spec/migrations/reset_severity_levels_to_new_default_spec.rb33
-rw-r--r--spec/migrations/retry_backfill_traversal_ids_spec.rb93
-rw-r--r--spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb10
-rw-r--r--spec/migrations/schedule_security_setting_creation_spec.rb58
-rw-r--r--spec/migrations/set_default_job_token_scope_true_spec.rb33
-rw-r--r--spec/migrations/set_email_confirmation_setting_before_removing_send_user_confirmation_email_column_spec.rb3
-rw-r--r--spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb2
-rw-r--r--spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb29
-rw-r--r--spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb102
-rw-r--r--spec/models/abuse_report_spec.rb35
-rw-r--r--spec/models/achievements/achievement_spec.rb2
-rw-r--r--spec/models/achievements/user_achievement_spec.rb2
-rw-r--r--spec/models/airflow/dags_spec.rb17
-rw-r--r--spec/models/analytics/cycle_analytics/aggregation_spec.rb10
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb58
-rw-r--r--spec/models/analytics/cycle_analytics/project_value_stream_spec.rb39
-rw-r--r--spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb8
-rw-r--r--spec/models/analytics/cycle_analytics/stage_spec.rb135
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb75
-rw-r--r--spec/models/appearance_spec.rb54
-rw-r--r--spec/models/approval_spec.rb2
-rw-r--r--spec/models/bulk_imports/entity_spec.rb69
-rw-r--r--spec/models/ci/bridge_spec.rb40
-rw-r--r--spec/models/ci/build_metadata_spec.rb4
-rw-r--r--spec/models/ci/build_pending_state_spec.rb9
-rw-r--r--spec/models/ci/build_spec.rb504
-rw-r--r--spec/models/ci/group_variable_spec.rb7
-rw-r--r--spec/models/ci/job_artifact_spec.rb31
-rw-r--r--spec/models/ci/job_token/allowlist_spec.rb39
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb29
-rw-r--r--spec/models/ci/job_token/scope_spec.rb165
-rw-r--r--spec/models/ci/pipeline_spec.rb322
-rw-r--r--spec/models/ci/processable_spec.rb10
-rw-r--r--spec/models/ci/runner_machine_spec.rb158
-rw-r--r--spec/models/ci/runner_spec.rb67
-rw-r--r--spec/models/ci/runner_version_spec.rb18
-rw-r--r--spec/models/ci/running_build_spec.rb2
-rw-r--r--spec/models/ci/secure_file_spec.rb5
-rw-r--r--spec/models/ci/trigger_spec.rb38
-rw-r--r--spec/models/ci/variable_spec.rb7
-rw-r--r--spec/models/ci_platform_metric_spec.rb2
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb157
-rw-r--r--spec/models/clusters/applications/cilium_spec.rb17
-rw-r--r--spec/models/clusters/cluster_spec.rb230
-rw-r--r--spec/models/commit_status_spec.rb98
-rw-r--r--spec/models/concerns/after_commit_queue_spec.rb2
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb2
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb2
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb34
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb116
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb8
-rw-r--r--spec/models/concerns/exportable_spec.rb236
-rw-r--r--spec/models/concerns/issuable_link_spec.rb14
-rw-r--r--spec/models/concerns/noteable_spec.rb65
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb2
-rw-r--r--spec/models/concerns/require_email_verification_spec.rb17
-rw-r--r--spec/models/concerns/sensitive_serializable_hash_spec.rb4
-rw-r--r--spec/models/concerns/spammable_spec.rb16
-rw-r--r--spec/models/concerns/taskable_spec.rb12
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb2
-rw-r--r--spec/models/container_registry/event_spec.rb71
-rw-r--r--spec/models/container_repository_spec.rb2
-rw-r--r--spec/models/cycle_analytics/project_level_stage_adapter_spec.rb9
-rw-r--r--spec/models/deploy_key_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb17
-rw-r--r--spec/models/design_management/design_spec.rb2
-rw-r--r--spec/models/discussion_spec.rb65
-rw-r--r--spec/models/environment_spec.rb37
-rw-r--r--spec/models/event_spec.rb2
-rw-r--r--spec/models/factories_spec.rb211
-rw-r--r--spec/models/group_spec.rb121
-rw-r--r--spec/models/hooks/project_hook_spec.rb120
-rw-r--r--spec/models/hooks/service_hook_spec.rb38
-rw-r--r--spec/models/hooks/system_hook_spec.rb12
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb31
-rw-r--r--spec/models/hooks/web_hook_spec.rb402
-rw-r--r--spec/models/incident_management/timeline_event_tag_spec.rb12
-rw-r--r--spec/models/integration_spec.rb8
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb30
-rw-r--r--spec/models/integrations/issue_tracker_data_spec.rb2
-rw-r--r--spec/models/integrations/jira_tracker_data_spec.rb2
-rw-r--r--spec/models/integrations/microsoft_teams_spec.rb4
-rw-r--r--spec/models/integrations/mock_ci_spec.rb4
-rw-r--r--spec/models/integrations/zentao_tracker_data_spec.rb2
-rw-r--r--spec/models/issue_email_participant_spec.rb6
-rw-r--r--spec/models/issue_spec.rb12
-rw-r--r--spec/models/jira_connect_installation_spec.rb8
-rw-r--r--spec/models/key_spec.rb8
-rw-r--r--spec/models/member_spec.rb16
-rw-r--r--spec/models/members/group_member_spec.rb8
-rw-r--r--spec/models/members/member_role_spec.rb26
-rw-r--r--spec/models/members/project_member_spec.rb22
-rw-r--r--spec/models/merge_request/cleanup_schedule_spec.rb2
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb2
-rw-r--r--spec/models/merge_request_diff_file_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb118
-rw-r--r--spec/models/ml/candidate_spec.rb69
-rw-r--r--spec/models/ml/experiment_spec.rb17
-rw-r--r--spec/models/namespace/traversal_hierarchy_spec.rb2
-rw-r--r--spec/models/namespace_setting_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb154
-rw-r--r--spec/models/namespaces/randomized_suffix_path_spec.rb37
-rw-r--r--spec/models/note_spec.rb42
-rw-r--r--spec/models/onboarding/learn_gitlab_spec.rb69
-rw-r--r--spec/models/packages/composer/metadatum_spec.rb29
-rw-r--r--spec/models/packages/debian/file_entry_spec.rb7
-rw-r--r--spec/models/packages/package_spec.rb28
-rw-r--r--spec/models/packages/tag_spec.rb12
-rw-r--r--spec/models/personal_access_token_spec.rb52
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb37
-rw-r--r--spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb14
-rw-r--r--spec/models/project_authorization_spec.rb15
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb18
-rw-r--r--spec/models/project_feature_spec.rb24
-rw-r--r--spec/models/project_import_state_spec.rb55
-rw-r--r--spec/models/project_setting_spec.rb50
-rw-r--r--spec/models/project_spec.rb241
-rw-r--r--spec/models/project_team_spec.rb2
-rw-r--r--spec/models/projects/data_transfer_spec.rb62
-rw-r--r--spec/models/protected_branch_spec.rb150
-rw-r--r--spec/models/protected_tag/create_access_level_spec.rb144
-rw-r--r--spec/models/release_highlight_spec.rb3
-rw-r--r--spec/models/release_spec.rb15
-rw-r--r--spec/models/repository_spec.rb97
-rw-r--r--spec/models/resource_event_spec.rb2
-rw-r--r--spec/models/resource_label_event_spec.rb2
-rw-r--r--spec/models/resource_milestone_event_spec.rb2
-rw-r--r--spec/models/resource_state_event_spec.rb2
-rw-r--r--spec/models/service_desk_setting_spec.rb65
-rw-r--r--spec/models/user_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb21
-rw-r--r--spec/models/wiki_directory_spec.rb9
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/models/work_item_spec.rb112
-rw-r--r--spec/models/work_items/type_spec.rb73
-rw-r--r--spec/models/work_items/widget_definition_spec.rb92
-rw-r--r--spec/models/work_items/widgets/assignees_spec.rb6
-rw-r--r--spec/models/work_items/widgets/hierarchy_spec.rb30
-rw-r--r--spec/models/work_items/widgets/labels_spec.rb6
-rw-r--r--spec/models/work_items/widgets/start_and_due_date_spec.rb6
-rw-r--r--spec/policies/ci/runner_policy_spec.rb23
-rw-r--r--spec/policies/global_policy_spec.rb100
-rw-r--r--spec/policies/group_policy_spec.rb174
-rw-r--r--spec/policies/issue_policy_spec.rb16
-rw-r--r--spec/policies/note_policy_spec.rb4
-rw-r--r--spec/policies/packages/policies/project_policy_spec.rb33
-rw-r--r--spec/policies/project_policy_spec.rb151
-rw-r--r--spec/policies/todo_policy_spec.rb2
-rw-r--r--spec/presenters/issue_email_participant_presenter_spec.rb59
-rw-r--r--spec/presenters/issue_presenter_spec.rb74
-rw-r--r--spec/presenters/project_presenter_spec.rb65
-rw-r--r--spec/presenters/user_presenter_spec.rb8
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb38
-rw-r--r--spec/requests/admin/background_migrations_controller_spec.rb2
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb8
-rw-r--r--spec/requests/api/api_spec.rb49
-rw-r--r--spec/requests/api/appearance_spec.rb43
-rw-r--r--spec/requests/api/applications_spec.rb77
-rw-r--r--spec/requests/api/avatar_spec.rb2
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/bulk_imports_spec.rb48
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb14
-rw-r--r--spec/requests/api/ci/jobs_spec.rb28
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb5
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb59
-rw-r--r--spec/requests/api/ci/runner/runners_reset_spec.rb5
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb100
-rw-r--r--spec/requests/api/ci/runners_spec.rb10
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb6
-rw-r--r--spec/requests/api/ci/variables_spec.rb129
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb12
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb41
-rw-r--r--spec/requests/api/discussions_spec.rb3
-rw-r--r--spec/requests/api/draft_notes_spec.rb178
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/graphql/boards/board_list_query_spec.rb18
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb1
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb30
-rw-r--r--spec/requests/api/graphql/ci/groups_spec.rb5
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb24
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb30
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb112
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb99
-rw-r--r--spec/requests/api/graphql/group/group_releases_spec.rb139
-rw-r--r--spec/requests/api/graphql/groups_query_spec.rb76
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb41
-rw-r--r--spec/requests/api/graphql/issues_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/achievements/create_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb151
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb177
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/notes/destroy_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/note_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb214
-rw-r--r--spec/requests/api/graphql/notes/note_spec.rb104
-rw-r--r--spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb58
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb108
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb224
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb23
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/created_spec.rb177
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb72
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/updated_spec.rb67
-rw-r--r--spec/requests/api/graphql/user_spec.rb2
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb21
-rw-r--r--spec/requests/api/group_variables_spec.rb105
-rw-r--r--spec/requests/api/internal/base_spec.rb6
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb2
-rw-r--r--spec/requests/api/invitations_spec.rb2
-rw-r--r--spec/requests/api/issue_links_spec.rb2
-rw-r--r--spec/requests/api/issues/issues_spec.rb31
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb12
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb12
-rw-r--r--spec/requests/api/maven_packages_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb12
-rw-r--r--spec/requests/api/namespaces_spec.rb9
-rw-r--r--spec/requests/api/notes_spec.rb3
-rw-r--r--spec/requests/api/project_attributes.yml13
-rw-r--r--spec/requests/api/project_events_spec.rb2
-rw-r--r--spec/requests/api/project_packages_spec.rb14
-rw-r--r--spec/requests/api/project_snippets_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb154
-rw-r--r--spec/requests/api/release/links_spec.rb12
-rw-r--r--spec/requests/api/releases_spec.rb69
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/api/users_preferences_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb39
-rw-r--r--spec/requests/groups/usage_quotas_controller_spec.rb2
-rw-r--r--spec/requests/jira_connect/public_keys_controller_spec.rb25
-rw-r--r--spec/requests/openid_connect_spec.rb6
-rw-r--r--spec/requests/profiles/keys_controller_spec.rb31
-rw-r--r--spec/requests/profiles/saved_replies_controller_spec.rb35
-rw-r--r--spec/requests/projects/airflow/dags_controller_spec.rb105
-rw-r--r--spec/requests/projects/blob_spec.rb87
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb84
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb152
-rw-r--r--spec/requests/projects/network_controller_spec.rb11
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb36
-rw-r--r--spec/requests/projects/pipelines_controller_spec.rb26
-rw-r--r--spec/requests/projects/releases_controller_spec.rb40
-rw-r--r--spec/requests/pwa_controller_spec.rb84
-rw-r--r--spec/requests/user_activity_spec.rb2
-rw-r--r--spec/requests/user_avatar_spec.rb2
-rw-r--r--spec/requests/verifies_with_email_spec.rb9
-rw-r--r--spec/routing/directs/milestone_spec.rb27
-rw-r--r--spec/routing/import_routing_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/doc_url_spec.rb69
-rw-r--r--spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb67
-rw-r--r--spec/rubocop/cop/migration/versioned_migration_class_spec.rb12
-rw-r--r--spec/rubocop/cop/rspec/env_mocking_spec.rb61
-rw-r--r--spec/rubocop/cop/rspec/invalid_feature_category_spec.rb120
-rw-r--r--spec/rubocop/cop/rspec/missing_feature_category_spec.rb31
-rw-r--r--spec/rubocop/cop/scalability/file_uploads_spec.rb2
-rw-r--r--spec/rubocop/migration_helpers_spec.rb24
-rw-r--r--spec/scripts/failed_tests_spec.rb200
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb2
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb2
-rw-r--r--spec/scripts/pipeline_test_report_builder_spec.rb191
-rw-r--r--spec/scripts/trigger-build_spec.rb23
-rw-r--r--spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb2
-rw-r--r--spec/serializers/ci/pipeline_entity_spec.rb3
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb3
-rw-r--r--spec/serializers/import/github_realtime_repo_entity_spec.rb39
-rw-r--r--spec/serializers/import/github_realtime_repo_serializer_spec.rb48
-rw-r--r--spec/serializers/integrations/field_entity_spec.rb6
-rw-r--r--spec/serializers/issue_entity_spec.rb56
-rw-r--r--spec/serializers/merge_requests/pipeline_entity_spec.rb3
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb3
-rw-r--r--spec/serializers/project_import_entity_spec.rb19
-rw-r--r--spec/services/achievements/create_service_spec.rb2
-rw-r--r--spec/services/analytics/cycle_analytics/stages/list_service_spec.rb7
-rw-r--r--spec/services/audit_event_service_spec.rb54
-rw-r--r--spec/services/authorized_project_update/project_access_changed_service_spec.rb11
-rw-r--r--spec/services/auto_merge_service_spec.rb2
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb227
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb67
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb141
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb17
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb8
-rw-r--r--spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb3
-rw-r--r--spec/services/ci/job_token_scope/add_project_service_spec.rb24
-rw-r--r--spec/services/ci/job_token_scope/remove_project_service_spec.rb4
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb14
-rw-r--r--spec/services/ci/parse_dotenv_artifact_service_spec.rb14
-rw-r--r--spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb250
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_schedules/update_service_spec.rb78
-rw-r--r--spec/services/ci/register_job_service_spec.rb1165
-rw-r--r--spec/services/ci/retry_job_service_spec.rb66
-rw-r--r--spec/services/ci/runners/create_runner_service_spec.rb135
-rw-r--r--spec/services/ci/runners/process_runner_version_update_service_spec.rb4
-rw-r--r--spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb32
-rw-r--r--spec/services/ci/runners/register_runner_service_spec.rb1
-rw-r--r--spec/services/ci/runners/stale_machines_cleanup_service_spec.rb45
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb2
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb2
-rw-r--r--spec/services/clusters/agents/refresh_authorization_service_spec.rb12
-rw-r--r--spec/services/concerns/rate_limited_service_spec.rb4
-rw-r--r--spec/services/event_create_service_spec.rb2
-rw-r--r--spec/services/export_csv/base_service_spec.rb31
-rw-r--r--spec/services/export_csv/map_export_fields_service_spec.rb55
-rw-r--r--spec/services/git/wiki_push_service_spec.rb2
-rw-r--r--spec/services/google_cloud/fetch_google_ip_list_service_spec.rb2
-rw-r--r--spec/services/groups/create_service_spec.rb10
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb4
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb86
-rw-r--r--spec/services/import_csv/base_service_spec.rb64
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb10
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb8
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb81
-rw-r--r--spec/services/issuable/destroy_service_spec.rb2
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb4
-rw-r--r--spec/services/issues/after_create_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb6
-rw-r--r--spec/services/issues/clone_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb22
-rw-r--r--spec/services/issues/create_service_spec.rb76
-rw-r--r--spec/services/issues/duplicate_service_spec.rb2
-rw-r--r--spec/services/issues/export_csv_service_spec.rb214
-rw-r--r--spec/services/issues/import_csv_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb4
-rw-r--r--spec/services/issues/referenced_merge_requests_service_spec.rb2
-rw-r--r--spec/services/issues/related_branches_service_spec.rb4
-rw-r--r--spec/services/issues/reopen_service_spec.rb6
-rw-r--r--spec/services/issues/reorder_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb68
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb2
-rw-r--r--spec/services/jira_connect_installations/update_service_spec.rb18
-rw-r--r--spec/services/keys/revoke_service_spec.rb48
-rw-r--r--spec/services/lfs/file_transformer_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb8
-rw-r--r--spec/services/members/base_service_spec.rb19
-rw-r--r--spec/services/members/destroy_service_spec.rb95
-rw-r--r--spec/services/members/projects/creator_service_spec.rb2
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb13
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb40
-rw-r--r--spec/services/merge_requests/export_csv_service_spec.rb18
-rw-r--r--spec/services/merge_requests/link_lfs_objects_service_spec.rb2
-rw-r--r--spec/services/merge_requests/pushed_branches_service_spec.rb2
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb2
-rw-r--r--spec/services/merge_requests/retarget_chain_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb32
-rw-r--r--spec/services/notes/create_service_spec.rb37
-rw-r--r--spec/services/notes/destroy_service_spec.rb8
-rw-r--r--spec/services/notification_service_spec.rb474
-rw-r--r--spec/services/packages/debian/create_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/debian/create_package_file_service_spec.rb31
-rw-r--r--spec/services/packages/debian/extract_changes_metadata_service_spec.rb11
-rw-r--r--spec/services/packages/debian/extract_deb_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/extract_metadata_service_spec.rb2
-rw-r--r--spec/services/packages/debian/find_or_create_incoming_service_spec.rb2
-rw-r--r--spec/services/packages/debian/find_or_create_package_service_spec.rb69
-rw-r--r--spec/services/packages/debian/generate_distribution_key_service_spec.rb2
-rw-r--r--spec/services/packages/debian/generate_distribution_service_spec.rb8
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb2
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb40
-rw-r--r--spec/services/packages/debian/process_package_file_service_spec.rb167
-rw-r--r--spec/services/packages/debian/sign_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/debian/update_distribution_service_spec.rb2
-rw-r--r--spec/services/pages/destroy_deployments_service_spec.rb22
-rw-r--r--spec/services/pages/migrate_from_legacy_storage_service_spec.rb2
-rw-r--r--spec/services/preview_markdown_service_spec.rb17
-rw-r--r--spec/services/projects/container_repository/destroy_service_spec.rb143
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb69
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/update_service_spec.rb2
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb21
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/projects/protect_default_branch_service_spec.rb32
-rw-r--r--spec/services/projects/transfer_service_spec.rb19
-rw-r--r--spec/services/protected_branches/create_service_spec.rb3
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb2
-rw-r--r--spec/services/quick_actions/target_service_spec.rb40
-rw-r--r--spec/services/releases/create_service_spec.rb71
-rw-r--r--spec/services/releases/update_service_spec.rb77
-rw-r--r--spec/services/resource_events/change_labels_service_spec.rb23
-rw-r--r--spec/services/security/ci_configuration/sast_create_service_spec.rb51
-rw-r--r--spec/services/serverless/associate_domain_service_spec.rb51
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb8
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/tasks_to_be_done/base_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb18
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/group_private_service_spec.rb2
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb54
-rw-r--r--spec/services/users/activity_service_spec.rb50
-rw-r--r--spec/services/users/assigned_issues_count_service_spec.rb2
-rw-r--r--spec/services/web_hook_service_spec.rb3
-rw-r--r--spec/services/work_items/build_service_spec.rb2
-rw-r--r--spec/services/work_items/create_service_spec.rb6
-rw-r--r--spec/services/work_items/delete_service_spec.rb2
-rw-r--r--spec/services/work_items/export_csv_service_spec.rb77
-rw-r--r--spec/services/work_items/update_service_spec.rb6
-rw-r--r--spec/spec_helper.rb100
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb4
-rw-r--r--spec/support/database/prevent_cross_joins.rb4
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/graphql/fake_tracer.rb4
-rw-r--r--spec/support/graphql/resolver_factories.rb4
-rw-r--r--spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb100
-rw-r--r--spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb41
-rw-r--r--spec/support/graphql/subscriptions/notes/helper.rb94
-rw-r--r--spec/support/helpers/ci/job_token_scope_helpers.rb61
-rw-r--r--spec/support/helpers/ci/template_helpers.rb23
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/database/multiple_databases_helpers.rb32
-rw-r--r--spec/support/helpers/email_helpers.rb20
-rw-r--r--spec/support/helpers/features/branches_helpers.rb9
-rw-r--r--spec/support/helpers/features/releases_helpers.rb7
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb4
-rw-r--r--spec/support/helpers/gitaly_setup.rb1
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb4
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb4
-rw-r--r--spec/support/helpers/listbox_helpers.rb8
-rw-r--r--spec/support/helpers/reload_helpers.rb4
-rw-r--r--spec/support/helpers/select2_helper.rb57
-rw-r--r--spec/support/helpers/stub_env.rb18
-rw-r--r--spec/support/helpers/stubbed_member.rb6
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/helpers/trial_status_widget_test_helper.rb9
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/import_export/common_util.rb14
-rw-r--r--spec/support/import_export/project_tree_expectations.rb2
-rw-r--r--spec/support/matchers/not_enqueue_mail_matcher.rb3
-rw-r--r--spec/support/matchers/schema_matcher.rb16
-rw-r--r--spec/support/models/ci/partitioning_testing/schema_helpers.rb19
-rw-r--r--spec/support/redis.rb6
-rw-r--r--spec/support/redis/redis_new_instance_shared_examples.rb50
-rw-r--r--spec/support/redis/redis_shared_examples.rb69
-rw-r--r--spec/support/rspec_order_todo.yml25
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb4
-rw-r--r--spec/support/shared_contexts/graphql/types/query_type_shared_context.rb45
-rw-r--r--spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb40
-rw-r--r--spec/support/shared_contexts/models/ci/job_token_scope.rb34
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb8
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/releases_and_group_releases_shared_context.rb60
-rw-r--r--spec/support/shared_examples/bulk_imports/visibility_level_examples.rb124
-rw-r--r--spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb55
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb9
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/incident_details_routing_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb141
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb127
-rw-r--r--spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb17
-rw-r--r--spec/support/shared_examples/lib/cache_helpers_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb272
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb64
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb177
-rw-r--r--spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb107
-rw-r--r--spec/support/shared_examples/models/exportable_shared_examples.rb73
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb5
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb40
-rw-r--r--spec/support/shared_examples/requests/admin_mode_shared_examples.rb98
-rw-r--r--spec/support/shared_examples/requests/api/debian_common_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb164
-rw-r--r--spec/support/shared_examples/requests/api/hooks_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/serializers/note_entity_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/export_csv/export_csv_invalid_fields_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/services/issuable_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb50
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/services/updating_mentions_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/views/themed_layout_examples.rb2
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb60
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb2
-rw-r--r--spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb14
-rw-r--r--spec/support/webmock.rb14
-rw-r--r--spec/support_specs/database/multiple_databases_helpers_spec.rb6
-rw-r--r--spec/support_specs/helpers/migrations_helpers_spec.rb8
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb34
-rw-r--r--spec/tasks/dev_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/background_migrations_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb12
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb218
-rw-r--r--spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db/validate_config_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb173
-rw-r--r--spec/tasks/gitlab/incoming_email_rake_spec.rb122
-rw-r--r--spec/tasks/gitlab/metrics_exporter_task_spec.rb (renamed from spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb)6
-rw-r--r--spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/seed/group_seed_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/service_desk_email_rake_spec.rb127
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb5
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb2
-rw-r--r--spec/tasks/import_rake_spec.rb110
-rw-r--r--spec/tasks/migrate/schema_check_rake_spec.rb5
-rw-r--r--spec/tooling/danger/config_files_spec.rb30
-rw-r--r--spec/tooling/danger/feature_flag_spec.rb70
-rw-r--r--spec/tooling/danger/product_intelligence_spec.rb47
-rw-r--r--spec/tooling/danger/project_helper_spec.rb10
-rw-r--r--spec/tooling/danger/specs_spec.rb71
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb137
-rw-r--r--spec/tooling/lib/tooling/helm3_client_spec.rb48
-rw-r--r--spec/tooling/lib/tooling/kubernetes_client_spec.rb20
-rw-r--r--spec/tooling/lib/tooling/mappings/base_spec.rb44
-rw-r--r--spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb169
-rw-r--r--spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb (renamed from spec/tooling/lib/tooling/view_to_js_mappings_spec.rb)49
-rw-r--r--spec/uploaders/object_storage/cdn/google_cdn_spec.rb5
-rw-r--r--spec/uploaders/object_storage/cdn_spec.rb18
-rw-r--r--spec/uploaders/object_storage/s3_spec.rb39
-rw-r--r--spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb7
-rw-r--r--spec/views/admin/application_settings/ci_cd.html.haml_spec.rb2
-rw-r--r--spec/views/groups/group_members/index.html.haml_spec.rb6
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb4
-rw-r--r--spec/views/layouts/header/_gitlab_version.html.haml_spec.rb3
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb37
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb55
-rw-r--r--spec/views/layouts/snippets.html.haml_spec.rb32
-rw-r--r--spec/views/notify/user_deactivated_email.html.haml_spec.rb56
-rw-r--r--spec/views/notify/user_deactivated_email.text.erb_spec.rb58
-rw-r--r--spec/views/profiles/keys/_key.html.haml_spec.rb79
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb28
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb4
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/issues/_issue.html.haml_spec.rb53
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb5
-rw-r--r--spec/views/projects/project_members/index.html.haml_spec.rb6
-rw-r--r--spec/views/projects/runners/_project_runners.html.haml_spec.rb (renamed from spec/views/projects/runners/_specific_runners.html.haml_spec.rb)10
-rw-r--r--spec/views/search/_results.html.haml_spec.rb32
-rw-r--r--spec/views/search/show.html.haml_spec.rb148
-rw-r--r--spec/views/shared/runners/_runner_details.html.haml_spec.rb2
-rw-r--r--spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb18
-rw-r--r--spec/views/shared/wikis/_sidebar.html.haml_spec.rb6
-rw-r--r--spec/workers/bulk_import_worker_spec.rb25
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb32
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb20
-rw-r--r--spec/workers/ci/cancel_redundant_pipelines_worker_spec.rb54
-rw-r--r--spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/ci/runners/stale_machines_cleanup_cron_worker_spec.rb42
-rw-r--r--spec/workers/concerns/application_worker_spec.rb9
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb32
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb2
-rw-r--r--spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb2
-rw-r--r--spec/workers/incident_management/close_incident_worker_spec.rb2
-rw-r--r--spec/workers/issues/close_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/close_issue_worker_spec.rb2
-rw-r--r--spec/workers/merge_requests/delete_source_branch_worker_spec.rb109
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb71
-rw-r--r--spec/workers/packages/debian/generate_distribution_worker_spec.rb8
-rw-r--r--spec/workers/packages/debian/process_changes_worker_spec.rb14
-rw-r--r--spec/workers/packages/debian/process_package_file_worker_spec.rb68
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb16
-rw-r--r--spec/workers/process_commit_worker_spec.rb6
-rw-r--r--spec/workers/projects/post_creation_worker_spec.rb7
-rw-r--r--spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb2
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb53
-rw-r--r--spec/workers/tasks_to_be_done/create_worker_spec.rb2
2063 files changed, 59471 insertions, 25449 deletions
diff --git a/spec/bin/feature_flag_spec.rb b/spec/bin/feature_flag_spec.rb
index cce103965d3..d1e4be5be28 100644
--- a/spec/bin/feature_flag_spec.rb
+++ b/spec/bin/feature_flag_spec.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'rspec-parameterized'
+require 'spec_helper'
load File.expand_path('../../bin/feature-flag', __dir__)
-RSpec.describe 'bin/feature-flag' do
+RSpec.describe 'bin/feature-flag', feature_category: :feature_flags do
using RSpec::Parameterized::TableSyntax
describe FeatureFlagCreator do
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb
index 7b64ad4a9b9..5bb3b69de01 100644
--- a/spec/config/application_spec.rb
+++ b/spec/config/application_spec.rb
@@ -3,31 +3,57 @@
require 'spec_helper'
RSpec.describe Gitlab::Application, feature_category: :scalability do # rubocop:disable RSpec/FilePath
- using RSpec::Parameterized::TableSyntax
+ describe 'config.filter_parameters' do
+ using RSpec::Parameterized::TableSyntax
- filtered_param = ActiveSupport::ParameterFilter::FILTERED
+ filtered = ActiveSupport::ParameterFilter::FILTERED
- context 'when parameters are logged' do
- describe 'rails does not leak confidential parameters' do
- def request_for_url(input_url)
- env = Rack::MockRequest.env_for(input_url)
- env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters
+ context 'when parameters are logged' do
+ describe 'rails does not leak confidential parameters' do
+ def request_for_url(input_url)
+ env = Rack::MockRequest.env_for(input_url)
+ env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters
- ActionDispatch::Request.new(env)
- end
+ ActionDispatch::Request.new(env)
+ end
+
+ where(:input_url, :output_query) do
+ '/' | {}
+ '/?safe=1' | { 'safe' => '1' }
+ '/?private_token=secret' | { 'private_token' => filtered }
+ '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => filtered }
+ '/?note=secret&noteable=1&prefix_note=2' | { 'note' => filtered, 'noteable' => '1', 'prefix_note' => '2' }
+ '/?note[note]=secret&target_type=1' | { 'note' => filtered, 'target_type' => '1' }
+ '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => filtered }, 'target_type' => '1' }
+ end
- where(:input_url, :output_query) do
- '/' | {}
- '/?safe=1' | { 'safe' => '1' }
- '/?private_token=secret' | { 'private_token' => filtered_param }
- '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => filtered_param }
- '/?note=secret&noteable=1&prefix_note=2' | { 'note' => filtered_param, 'noteable' => '1', 'prefix_note' => '2' }
- '/?note[note]=secret&target_type=1' | { 'note' => filtered_param, 'target_type' => '1' }
- '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => filtered_param }, 'target_type' => '1' }
+ with_them do
+ it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) }
+ end
end
+ end
+ end
+
+ describe 'clear_active_connections_again initializer' do
+ subject(:clear_active_connections_again) do
+ described_class.initializers.find { |i| i.name == :clear_active_connections_again }
+ end
+
+ it 'is included in list of Rails initializers' do
+ expect(clear_active_connections_again).to be_present
+ end
+
+ it 'is configured after set_routes_reloader_hook' do
+ expect(clear_active_connections_again.after).to eq(:set_routes_reloader_hook)
+ end
+
+ describe 'functionality', :reestablished_active_record_base do
+ it 'clears all connections' do
+ Project.first
+
+ clear_active_connections_again.run
- with_them do
- it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) }
+ expect(ActiveRecord::Base.connection_handler.active_connections?).to eq(false)
end
end
end
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index 4917b043812..0928f2b72ff 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Settings, feature_category: :authentication_and_authorization do
+ using RSpec::Parameterized::TableSyntax
+
describe 'omniauth' do
it 'defaults to enabled' do
expect(described_class.omniauth.enabled).to be true
@@ -15,6 +17,32 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do
end
end
+ describe '.build_ci_component_fqdn' do
+ subject(:fqdn) { described_class.build_ci_component_fqdn }
+
+ where(:host, :port, :relative_url, :result) do
+ 'acme.com' | 9090 | '/gitlab' | 'acme.com:9090/gitlab/'
+ 'acme.com' | 443 | '/gitlab' | 'acme.com/gitlab/'
+ 'acme.com' | 443 | '' | 'acme.com/'
+ 'acme.com' | 9090 | '' | 'acme.com:9090/'
+ 'test' | 9090 | '' | 'test:9090/'
+ end
+
+ with_them do
+ before do
+ allow(Gitlab.config).to receive(:gitlab).and_return(
+ Settingslogic.new({
+ 'host' => host,
+ 'https' => true,
+ 'port' => port,
+ 'relative_url_root' => relative_url
+ }))
+ end
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
describe '.attr_encrypted_db_key_base_truncated' do
it 'is a string with maximum 32 bytes size' do
expect(described_class.attr_encrypted_db_key_base_truncated.bytesize)
@@ -162,4 +190,12 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do
expect(described_class.microsoft_graph_mailer.graph_endpoint).to eq('https://graph.microsoft.com')
end
end
+
+ describe '.repositories' do
+ it 'sets up storage settings' do
+ described_class.repositories.storages.each do |_, storage|
+ expect(storage).to be_a Gitlab::GitalyClient::StorageSettings
+ end
+ end
+ end
end
diff --git a/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json b/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json
index 01c6563f76a..7d96b5faea3 100644
--- a/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json
+++ b/spec/contracts/contracts/project/pipelines/index/pipelines#index-get_list_project_pipelines.json
@@ -329,9 +329,6 @@
"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)$"
},
- "$.body.pipelines[*].details.name": {
- "match": "type"
- },
"$.body.pipelines[*].details.manual_actions": {
"min": 1
},
diff --git a/spec/controllers/admin/application_settings/appearances_controller_spec.rb b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
index 78dce4558c3..f21c93e85d2 100644
--- a/spec/controllers/admin/application_settings/appearances_controller_spec.rb
+++ b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
@@ -11,8 +11,10 @@ RSpec.describe Admin::ApplicationSettings::AppearancesController do
let(:create_params) do
{
title: 'Foo',
- pwa_short_name: 'F',
description: 'Bar',
+ pwa_name: 'GitLab PWA',
+ pwa_short_name: 'F',
+ pwa_description: 'This is GitLab as PWA',
header_message: header_message,
footer_message: footer_message
}
@@ -26,6 +28,11 @@ RSpec.describe Admin::ApplicationSettings::AppearancesController do
post :create, params: { appearance: create_params }
expect(Appearance.current).to have_attributes(
+ title: 'Foo',
+ description: 'Bar',
+ pwa_name: 'GitLab PWA',
+ pwa_short_name: 'F',
+ pwa_description: 'This is GitLab as PWA',
header_message: header_message,
footer_message: footer_message,
email_header_and_footer_enabled: false,
@@ -41,6 +48,11 @@ RSpec.describe Admin::ApplicationSettings::AppearancesController do
post :create, params: { appearance: create_params }
expect(Appearance.current).to have_attributes(
+ title: 'Foo',
+ description: 'Bar',
+ pwa_name: 'GitLab PWA',
+ pwa_short_name: 'F',
+ pwa_description: 'This is GitLab as PWA',
header_message: header_message,
footer_message: footer_message,
email_header_and_footer_enabled: true
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index 86a4ac61194..8e62aeed7d0 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::ClustersController do
+RSpec.describe Admin::ClustersController, feature_category: :kubernetes_management do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
diff --git a/spec/controllers/admin/instance_review_controller_spec.rb b/spec/controllers/admin/instance_review_controller_spec.rb
index 342562618b2..6eab135b3a6 100644
--- a/spec/controllers/admin/instance_review_controller_spec.rb
+++ b/spec/controllers/admin/instance_review_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::InstanceReviewController do
+RSpec.describe Admin::InstanceReviewController, feature_category: :service_ping do
include UsageDataHelpers
let(:admin) { create(:admin) }
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 6d58abb9d4d..a39a1f38a11 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -34,6 +34,33 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
end
end
+ describe '#new' do
+ context 'when create_runner_workflow is enabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: true)
+ end
+
+ it 'renders a :new template' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:new)
+ end
+ end
+
+ context 'when create_runner_workflow is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: false)
+ end
+
+ it 'returns :not_found' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe '#edit' do
render_views
@@ -105,49 +132,6 @@ RSpec.describe Admin::RunnersController, feature_category: :runner_fleet do
end
end
- describe '#destroy' do
- it 'destroys the runner' do
- expect_next_instance_of(Ci::Runners::UnregisterRunnerService, runner, user) do |service|
- expect(service).to receive(:execute).once.and_call_original
- end
-
- delete :destroy, params: { id: runner.id }
-
- expect(response).to have_gitlab_http_status(:found)
- expect(Ci::Runner.find_by(id: runner.id)).to be_nil
- end
- end
-
- describe '#resume' do
- it 'marks the runner as active and ticks the queue' do
- runner.update!(active: false)
-
- expect do
- post :resume, params: { id: runner.id }
- end.to change { runner.ensure_runner_queue_value }
-
- runner.reload
-
- expect(response).to have_gitlab_http_status(:found)
- expect(runner.active).to eq(true)
- end
- end
-
- describe '#pause' do
- it 'marks the runner as inactive and ticks the queue' do
- runner.update!(active: true)
-
- expect do
- post :pause, params: { id: runner.id }
- end.to change { runner.ensure_runner_queue_value }
-
- runner.reload
-
- expect(response).to have_gitlab_http_status(:found)
- expect(runner.active).to eq(false)
- end
- end
-
describe 'GET #runner_setup_scripts' do
it 'renders the setup scripts' do
get :runner_setup_scripts, params: { os: 'linux', arch: 'amd64' }
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index eecb803fb1a..63e68118066 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -481,37 +481,22 @@ RSpec.describe Admin::UsersController do
end
describe 'PUT ban/:id', :aggregate_failures do
- context 'when ban_user_feature_flag is enabled' do
- it 'bans user' do
- put :ban, params: { id: user.username }
-
- expect(user.reload.banned?).to be_truthy
- expect(flash[:notice]).to eq _('Successfully banned')
- end
-
- context 'when unsuccessful' do
- let(:user) { create(:user, :blocked) }
+ it 'bans user' do
+ put :ban, params: { id: user.username }
- it 'does not ban user' do
- put :ban, params: { id: user.username }
-
- user.reload
- expect(user.banned?).to be_falsey
- expect(flash[:alert]).to eq _('Error occurred. User was not banned')
- end
- end
+ expect(user.reload.banned?).to be_truthy
+ expect(flash[:notice]).to eq _('Successfully banned')
end
- context 'when ban_user_feature_flag is not enabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
+ context 'when unsuccessful' do
+ let(:user) { create(:user, :blocked) }
- it 'does not ban user, renders 404' do
+ it 'does not ban user' do
put :ban, params: { id: user.username }
- expect(user.reload.banned?).to be_falsey
- expect(response).to have_gitlab_http_status(:not_found)
+ user.reload
+ expect(user.banned?).to be_falsey
+ expect(flash[:alert]).to eq _('Error occurred. User was not banned')
end
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index e9b39d44e46..97729d181b1 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe AutocompleteController do
let(:project) { create(:project) }
let(:user) { project.first_owner }
- context 'GET users' do
+ context 'GET users', feature_category: :user_management do
let!(:user2) { create(:user) }
let!(:non_member) { create(:user) }
@@ -248,7 +248,7 @@ RSpec.describe AutocompleteController do
end
end
- context 'GET projects' do
+ context 'GET projects', feature_category: :projects do
let(:authorized_project) { create(:project) }
let(:authorized_search_project) { create(:project, name: 'rugged') }
@@ -339,7 +339,7 @@ RSpec.describe AutocompleteController do
end
end
- context 'GET award_emojis' do
+ context 'GET award_emojis', feature_category: :team_planning do
let(:user2) { create(:user) }
let!(:award_emoji1) { create_list(:award_emoji, 2, user: user, name: 'thumbsup') }
let!(:award_emoji2) { create_list(:award_emoji, 1, user: user, name: 'thumbsdown') }
@@ -377,7 +377,7 @@ RSpec.describe AutocompleteController do
end
end
- context 'GET deploy_keys_with_owners' do
+ context 'GET deploy_keys_with_owners', feature_category: :continuous_delivery do
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:deploy_key) { create(:deploy_key, user: user) }
@@ -451,7 +451,7 @@ RSpec.describe AutocompleteController do
end
end
- context 'Get merge_request_target_branches' do
+ context 'Get merge_request_target_branches', feature_category: :code_review_workflow do
let!(:merge_request) { create(:merge_request, source_project: project, target_branch: 'feature') }
context 'anonymous user' do
diff --git a/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
new file mode 100644
index 00000000000..246119a8118
--- /dev/null
+++ b/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStreamActions, type: :controller,
+feature_category: :planning_analytics do
+ subject(:controller) do
+ Class.new(ApplicationController) do
+ include Analytics::CycleAnalytics::ValueStreamActions
+
+ def call_namespace
+ namespace
+ end
+ end
+ end
+
+ describe '#namespace' do
+ it 'raises NotImplementedError' do
+ expect { controller.new.call_namespace }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb
index 28b79a10624..12b4065b89c 100644
--- a/spec/controllers/concerns/product_analytics_tracking_spec.rb
+++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb
@@ -2,22 +2,24 @@
require "spec_helper"
-RSpec.describe ProductAnalyticsTracking, :snowplow do
+RSpec.describe ProductAnalyticsTracking, :snowplow, feature_category: :product_analytics do
include TrackingHelpers
include SnowplowHelpers
let(:user) { create(:user) }
+ let(:event_name) { 'an_event' }
let!(:group) { create(:group) }
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ stub_const("#{described_class}::MIGRATED_EVENTS", ['an_event'])
end
controller(ApplicationController) do
include ProductAnalyticsTracking
skip_before_action :authenticate_user!, only: :show
- track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow],
+ track_event(:index, :show, name: 'an_event', destinations: [:redis_hll, :snowplow],
conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
def index
@@ -53,16 +55,16 @@ RSpec.describe ProductAnalyticsTracking, :snowplow do
def expect_redis_hll_tracking
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to have_received(:track_event)
- .with('g_analytics_valuestream', values: instance_of(String))
+ .with(event_name, values: instance_of(String))
end
def expect_snowplow_tracking(user)
- context = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'g_analytics_valuestream')
+ context = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name)
.to_context.to_json
expect_snowplow_event(
category: anything,
- action: 'g_analytics_valuestream',
+ action: event_name,
namespace: group,
user: user,
context: [context]
@@ -89,7 +91,9 @@ RSpec.describe ProductAnalyticsTracking, :snowplow do
context 'when FF is disabled' do
before do
- stub_feature_flags(route_hll_to_snowplow: false)
+ stub_const("#{described_class}::MIGRATED_EVENTS", [])
+ allow(Feature).to receive(:enabled?).and_call_original
+ allow(Feature).to receive(:enabled?).with('route_hll_to_snowplow', anything).and_return(false)
end
it 'doesnt track snowplow event' do
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 0b24387483b..6acbff6e745 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -54,17 +54,18 @@ RSpec.describe SendFileUpload do
FileUtils.rm_f(temp_file)
end
- shared_examples 'handles image resize requests' do
+ shared_examples 'handles image resize requests' do |mount|
let(:headers) { double }
let(:image_requester) { build(:user) }
let(:image_owner) { build(:user) }
+ let(:width) { mount == :pwa_icon ? 192 : 64 }
let(:params) do
{ attachment: 'avatar.png' }
end
before do
allow(uploader).to receive(:image_safe_for_scaling?).and_return(true)
- allow(uploader).to receive(:mounted_as).and_return(:avatar)
+ allow(uploader).to receive(:mounted_as).and_return(mount)
allow(controller).to receive(:headers).and_return(headers)
# both of these are valid cases, depending on whether we are dealing with
@@ -99,11 +100,11 @@ RSpec.describe SendFileUpload do
context 'with valid width parameter' do
it 'renders OK with workhorse command header' do
expect(controller).not_to receive(:send_file)
- expect(controller).to receive(:params).at_least(:once).and_return(width: '64')
+ expect(controller).to receive(:params).at_least(:once).and_return(width: width.to_s)
expect(controller).to receive(:head).with(:ok)
expect(Gitlab::Workhorse).to receive(:send_scaled_image)
- .with(a_string_matching('^(/.+|https://.+)'), 64, 'image/png')
+ .with(a_string_matching('^(/.+|https://.+)'), width, 'image/png')
.and_return([Gitlab::Workhorse::SEND_DATA_HEADER, "send-scaled-img:faux"])
expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, "send-scaled-img:faux")
@@ -168,7 +169,8 @@ RSpec.describe SendFileUpload do
subject
end
- it_behaves_like 'handles image resize requests'
+ it_behaves_like 'handles image resize requests', :avatar
+ it_behaves_like 'handles image resize requests', :pwa_icon
end
context 'with inline image' do
@@ -273,7 +275,8 @@ RSpec.describe SendFileUpload do
end
end
- it_behaves_like 'handles image resize requests'
+ it_behaves_like 'handles image resize requests', :avatar
+ it_behaves_like 'handles image resize requests', :pwa_icon
end
context 'when CDN-enabled remote file is used' do
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index b4a4ac56fce..e8ee146a13a 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
+RSpec.describe Dashboard::ProjectsController, :aggregate_failures, feature_category: :projects do
include ExternalAuthorizationServiceHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index ea12b0c5ad7..304e08f40bd 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DashboardController do
+RSpec.describe DashboardController, feature_category: :code_review_workflow do
context 'signed in' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
@@ -46,6 +46,64 @@ RSpec.describe DashboardController do
describe 'GET merge requests' do
it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
it_behaves_like 'issuables requiring filter', :merge_requests
+
+ context 'when an ActiveRecord::QueryCanceled is raised' do
+ before do
+ allow_next_instance_of(Gitlab::IssuableMetadata) do |instance|
+ allow(instance).to receive(:data).and_raise(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ it 'sets :search_timeout_occurred' do
+ get :merge_requests, params: { author_id: user.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:search_timeout_occurred)).to eq(true)
+ end
+
+ context 'rendering views' do
+ render_views
+
+ it 'shows error message' do
+ get :merge_requests, params: { author_id: user.id }
+
+ expect(response.body).to have_content('Too many results to display. Edit your search or add a filter.')
+ end
+
+ it 'does not display MR counts in nav' do
+ get :merge_requests, params: { author_id: user.id }
+
+ expect(response.body).to have_content('Open Merged Closed All')
+ expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
+ end
+ end
+
+ it 'logs the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
+
+ get :merge_requests, params: { author_id: user.id }
+ end
+ end
+
+ context 'when an ActiveRecord::QueryCanceled is not raised' do
+ it 'does not set :search_timeout_occurred' do
+ get :merge_requests, params: { author_id: user.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:search_timeout_occurred)).to eq(nil)
+ end
+
+ context 'rendering views' do
+ render_views
+
+ it 'displays MR counts in nav' do
+ get :merge_requests, params: { author_id: user.id }
+
+ expect(response.body).to have_content('Open 0 Merged 0 Closed 0 All 0')
+ expect(response.body).not_to have_content('Open Merged Closed All')
+ end
+ end
+ end
end
end
@@ -53,9 +111,9 @@ RSpec.describe DashboardController do
include DesignManagementTestHelpers
render_views
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
- let(:other_project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) }
+ let_it_be(:other_project) { create(:project, :public) }
before do
enable_design_management
@@ -76,22 +134,53 @@ RSpec.describe DashboardController do
other_project.add_developer(user)
end
- it 'returns count' do
- get :activity, params: { format: :json }
+ context 'without filter param' do
+ it 'returns only events of the user' do
+ get :activity, params: { format: :json }
+
+ expect(json_response['count']).to eq(3)
+ end
+ end
+
+ context 'with "projects" filter' do
+ it 'returns events of the user\'s projects' do
+ get :activity, params: { format: :json, filter: :projects }
- expect(json_response['count']).to eq(6)
+ expect(json_response['count']).to eq(6)
+ end
+ end
+
+ context 'with "followed" filter' do
+ let_it_be(:followed_user) { create(:user) }
+ let_it_be(:followed_user_private_project) { create(:project, :private) }
+ let_it_be(:followed_user_public_project) { create(:project, :public) }
+
+ before do
+ followed_user_private_project.add_developer(followed_user)
+ followed_user_public_project.add_developer(followed_user)
+ user.follow(followed_user)
+ create(:event, :created, project: followed_user_private_project, target: create(:issue),
+ author: followed_user)
+ create(:event, :created, project: followed_user_public_project, target: create(:issue), author: followed_user)
+ end
+
+ it 'returns public events of the user\'s followed users' do
+ get :activity, params: { format: :json, filter: :followed }
+
+ expect(json_response['count']).to eq(1)
+ end
end
end
context 'when user has no permission to see the event' do
it 'filters out invisible event' do
- get :activity, params: { format: :json }
+ get :activity, params: { format: :json, filter: :projects }
expect(json_response['html']).to include(_('No activities found'))
end
it 'filters out invisible event when calculating the count' do
- get :activity, params: { format: :json }
+ get :activity, params: { format: :json, filter: :projects }
expect(json_response['count']).to eq(0)
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index a79d9fa1276..c4f0feb21e2 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Explore::ProjectsController do
+RSpec.describe Explore::ProjectsController, feature_category: :projects do
shared_examples 'explore projects' do
let(:expected_default_sort) { 'latest_activity_desc' }
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 75f281caa90..7aad67b01e8 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GraphqlController do
+RSpec.describe GraphqlController, feature_category: :integrations do
include GraphqlHelpers
# two days is enough to make timezones irrelevant
@@ -329,11 +329,24 @@ RSpec.describe GraphqlController do
expect(assigns(:context)[:request]).to eq request
end
+
+ it 'sets `context[:remove_deprecated]` to false by default' do
+ post :execute
+
+ expect(assigns(:context)[:remove_deprecated]).to be false
+ end
+
+ it 'sets `context[:remove_deprecated]` to true when `remove_deprecated` param is truthy' do
+ post :execute, params: { remove_deprecated: '1' }
+
+ expect(assigns(:context)[:remove_deprecated]).to be true
+ end
end
describe 'Admin Mode' do
- let(:admin) { create(:admin) }
- let(:project) { create(:project) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project) }
+
let(:graphql_query) { graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name)) }
before do
@@ -431,114 +444,4 @@ RSpec.describe GraphqlController do
expect(log_payload.dig(:exception_object)).to eq(exception)
end
end
-
- describe 'removal of deprecated items' do
- let(:mock_schema) do
- Class.new(GraphQL::Schema) do
- lazy_resolve ::Gitlab::Graphql::Lazy, :force
-
- query(Class.new(::Types::BaseObject) do
- graphql_name 'Query'
-
- field :foo, GraphQL::Types::Boolean,
- deprecated: { milestone: '0.1', reason: :renamed }
-
- field :bar, (Class.new(::Types::BaseEnum) do
- graphql_name 'BarEnum'
-
- value 'FOOBAR', value: 'foobar', deprecated: { milestone: '0.1', reason: :renamed }
- end)
-
- field :baz, GraphQL::Types::Boolean do
- argument :arg, String, required: false, deprecated: { milestone: '0.1', reason: :renamed }
- end
-
- def foo
- false
- end
-
- def bar
- 'foobar'
- end
-
- def baz(arg:)
- false
- end
- end)
- end
- end
-
- before do
- allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args|
- mock_schema.execute(*args)
- end
- end
-
- context 'without `remove_deprecated` param' do
- let(:params) { { query: '{ foo bar baz(arg: "test") }' } }
-
- subject { post :execute, params: params }
-
- it "sets context's `remove_deprecated` value to false" do
- subject
-
- expect(assigns(:context)[:remove_deprecated]).to be false
- end
-
- it 'returns deprecated items in response' do
- subject
-
- expect(json_response).to include('data' => { 'foo' => false, 'bar' => 'FOOBAR', 'baz' => false })
- end
- end
-
- context 'with `remove_deprecated` param' do
- let(:params) { { remove_deprecated: 'true' } }
-
- subject { post :execute, params: params }
-
- it "sets context's `remove_deprecated` value to true" do
- subject
-
- expect(assigns(:context)[:remove_deprecated]).to be true
- end
-
- it 'does not allow deprecated field' do
- params[:query] = '{ foo }'
-
- subject
-
- expect(json_response).not_to include('data' => { 'foo' => false })
- expect(json_response).to include(
- 'errors' => include(a_hash_including('message' => /Field 'foo' doesn't exist on type 'Query'/))
- )
- end
-
- it 'does not allow deprecated enum value' do
- params[:query] = '{ bar }'
-
- subject
-
- expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
- expect(json_response).to include(
- 'errors' => include(
- a_hash_including(
- 'message' => /`Query.bar` returned `"foobar"` at `bar`, but this isn't a valid value for `BarEnum`/
- )
- )
- )
- end
-
- it 'does not allow deprecated argument' do
- params[:query] = '{ baz(arg: "test") }'
-
- subject
-
- expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
- expect(json_response).to include(
- 'errors' => include(a_hash_including('message' => /Field 'baz' doesn't accept argument 'arg'/))
- )
- end
- end
- end
end
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index f05551432fa..d0656ee47ce 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::ChildrenController do
+RSpec.describe Groups::ChildrenController, feature_category: :subgroups do
include ExternalAuthorizationServiceHelpers
let(:group) { create(:group, :public) }
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 46f507c34ba..01ea7101f2e 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::ClustersController do
+RSpec.describe Groups::ClustersController, feature_category: :kubernetes_management do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index 0521c5e02a8..916b2cf10dd 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::LabelsController do
+RSpec.describe Groups::LabelsController, 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/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 93c1571bb6c..1a60f7d824e 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -30,7 +30,6 @@ RSpec.describe Groups::RunnersController, feature_category: :runner_fleet do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
- expect(assigns(:group_runners_limited_count)).to be(2)
end
it 'tracks the event' do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 22a406b3197..9184cd2263e 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupsController, factory_default: :keep do
+RSpec.describe GroupsController, factory_default: :keep, feature_category: :code_review_workflow do
include ExternalAuthorizationServiceHelpers
include AdminModeHelper
@@ -552,6 +552,68 @@ RSpec.describe GroupsController, factory_default: :keep do
expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
end
end
+
+ context 'rendering views' do
+ render_views
+
+ it 'displays MR counts in nav' do
+ get :merge_requests, params: { id: group.to_param }
+
+ expect(response.body).to have_content('Open 2 Merged 0 Closed 0 All 2')
+ expect(response.body).not_to have_content('Open Merged Closed All')
+ end
+
+ context 'when MergeRequestsFinder raises an exception' do
+ before do
+ allow_next_instance_of(MergeRequestsFinder) do |instance|
+ allow(instance).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ it 'does not display MR counts in nav' do
+ get :merge_requests, params: { id: group.to_param }
+
+ expect(response.body).to have_content('Open Merged Closed All')
+ expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
+ end
+ end
+ end
+
+ context 'when an ActiveRecord::QueryCanceled is raised' do
+ before do
+ allow_next_instance_of(Gitlab::IssuableMetadata) do |instance|
+ allow(instance).to receive(:data).and_raise(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ it 'sets :search_timeout_occurred' do
+ get :merge_requests, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:search_timeout_occurred)).to eq(true)
+ end
+
+ it 'logs the exception' do
+ get :merge_requests, params: { id: group.to_param }
+ end
+
+ context 'rendering views' do
+ render_views
+
+ it 'shows error message' do
+ get :merge_requests, params: { id: group.to_param }
+
+ expect(response.body).to have_content('Too many results to display. Edit your search or add a filter.')
+ end
+
+ it 'does not display MR counts in nav' do
+ get :merge_requests, params: { id: group.to_param }
+
+ expect(response.body).to have_content('Open Merged Closed All')
+ expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0')
+ end
+ end
+ end
end
describe 'DELETE #destroy' do
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index a0d5b576e74..a3992ae850e 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -9,14 +9,12 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
stub_application_setting(bulk_import_enabled: true)
sign_in(user)
+
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
context 'when user is signed in' do
context 'when bulk_import feature flag is enabled' do
- before do
- stub_feature_flags(bulk_import: true)
- end
-
describe 'POST configure' do
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
@@ -400,6 +398,18 @@ RSpec.describe Import::BulkImportsController, feature_category: :importers do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
+
+ context 'when request exceeds rate limits' do
+ it 'prevents user from starting a new migration' do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+
+ post :create, params: { bulk_import: {} }
+
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+ end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index c1a61a78d80..406a3604b23 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Import::GithubController, feature_category: :import do
+RSpec.describe Import::GithubController, feature_category: :importers do
include ImportSpecHelper
let(:provider) { :github }
@@ -375,7 +375,9 @@ RSpec.describe Import::GithubController, feature_category: :import do
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')
+ project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo')
+
+ ::Gitlab::GithubImport::ObjectCounter.increment(project, :issue, :imported, value: 8)
get :realtime_changes
@@ -417,4 +419,56 @@ RSpec.describe Import::GithubController, feature_category: :import do
end
end
end
+
+ describe 'POST cancel_all' do
+ context 'when import is in progress' do
+ it 'returns success' do
+ project = create(:project, :import_scheduled, namespace: user.namespace, import_type: 'github', import_url: 'https://fake.url')
+ project2 = create(:project, :import_started, namespace: user.namespace, import_type: 'github', import_url: 'https://fake2.url')
+
+ expect(Import::Github::CancelProjectImportService)
+ .to receive(:new).with(project, user)
+ .and_return(double(execute: { status: :success, project: project }))
+
+ expect(Import::Github::CancelProjectImportService)
+ .to receive(:new).with(project2, user)
+ .and_return(double(execute: { status: :bad_request, message: 'The import cannot be canceled because it is finished' }))
+
+ post :cancel_all
+
+ expect(json_response).to eq([
+ {
+ 'id' => project.id,
+ 'status' => 'success'
+ },
+ {
+ 'id' => project2.id,
+ 'status' => 'bad_request',
+ 'error' => 'The import cannot be canceled because it is finished'
+ }
+ ])
+ end
+ end
+
+ context 'when there is no imports in progress' do
+ it 'returns an empty array' do
+ create(:project, :import_finished, namespace: user.namespace, import_type: 'github', import_url: 'https://fake.url')
+
+ post :cancel_all
+
+ expect(json_response).to eq([])
+ end
+ end
+
+ context 'when there is no projects created by user' do
+ it 'returns an empty array' do
+ other_user_project = create(:project, :import_started, import_type: 'github', import_url: 'https://fake.url')
+
+ post :cancel_all
+
+ expect(json_response).to eq([])
+ expect(other_user_project.import_status).to eq('started')
+ end
+ end
+ end
end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
index 1b4b67eeaff..ba349768b0f 100644
--- a/spec/controllers/profiles/accounts_controller_spec.rb
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Profiles::AccountsController do
end
end
- [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq, :dingtalk, :alicloud].each do |provider|
+ [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :dingtalk, :alicloud].each do |provider|
describe "#{provider} provider" do
let(:user) { create(:omniauth_user, provider: provider.to_s) }
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 1dd564427d3..7d7cdededdb 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::TwoFactorAuthsController do
+RSpec.describe Profiles::TwoFactorAuthsController, feature_category: :authentication_and_authorization do
before do
# `user` should be defined within the action-specific describe blocks
sign_in(user)
@@ -31,7 +31,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do
shared_examples 'user must enter a valid current password' do
let(:current_password) { '123' }
- let(:error_message) { { message: _('You must provide a valid current password') } }
+ let(:error_message) { { message: _('You must provide a valid current password.') } }
it 'requires the current password', :aggregate_failures do
go
@@ -154,7 +154,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do
context 'with valid pin' do
before do
- expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
+ allow(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end
it 'enables 2fa for the user' do
@@ -187,6 +187,64 @@ RSpec.describe Profiles::TwoFactorAuthsController do
it 'renders create' do
go
expect(response).to render_template(:create)
+ expect(user.otp_backup_codes?).to be_eql(true)
+ end
+
+ it 'do not create new backup codes if exists' do
+ expect(user).to receive(:otp_backup_codes?).and_return(true)
+ go
+ expect(response).to redirect_to(profile_two_factor_auth_path)
+ end
+
+ it 'calls to delete other sessions when backup codes already exist' do
+ expect(user).to receive(:otp_backup_codes?).and_return(true)
+ expect(ActiveSession).to receive(:destroy_all_but_current)
+ go
+ end
+
+ context 'when webauthn_without_totp flag is disabled' do
+ before do
+ stub_feature_flags(webauthn_without_totp: false)
+ expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
+ end
+
+ it 'enables 2fa for the user' do
+ go
+
+ user.reload
+ expect(user).to be_two_factor_enabled
+ end
+
+ it 'presents plaintext codes for the user to save' do
+ expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c))
+
+ go
+
+ expect(assigns[:codes]).to match_array %w(a b c)
+ end
+
+ it 'calls to delete other sessions' do
+ expect(ActiveSession).to receive(:destroy_all_but_current)
+
+ go
+ end
+
+ it 'dismisses the `TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK` callout' do
+ expect(controller.helpers).to receive(:dismiss_two_factor_auth_recovery_settings_check)
+
+ go
+ end
+
+ it 'renders create' do
+ go
+ expect(response).to render_template(:create)
+ end
+
+ it 'renders create even if backup code already exists' do
+ expect(user).to receive(:otp_backup_codes?).and_return(true)
+ go
+ expect(response).to render_template(:create)
+ end
end
end
@@ -254,6 +312,75 @@ RSpec.describe Profiles::TwoFactorAuthsController do
end
end
+ describe 'POST create_webauthn' do
+ let_it_be_with_reload(:user) { create(:user) }
+ let(:client) { WebAuthn::FakeClient.new('http://localhost', encoding: :base64) }
+ let(:credential) { create_credential(client: client, rp_id: request.host) }
+
+ let(:params) { { device_registration: { name: 'touch id', device_response: device_response } } } # rubocop:disable Rails/SaveBang
+
+ let(:params_with_password) { { device_registration: { name: 'touch id', device_response: device_response }, current_password: user.password } } # rubocop:disable Rails/SaveBang
+
+ before do
+ session[:challenge] = challenge
+ end
+
+ def go
+ post :create_webauthn, params: params
+ end
+
+ def challenge
+ @_challenge ||= begin
+ options_for_create = WebAuthn::Credential.options_for_create(
+ user: { id: user.webauthn_xid, name: user.username },
+ authenticator_selection: { user_verification: 'discouraged' },
+ rp: { name: 'GitLab' }
+ )
+ options_for_create.challenge
+ end
+ end
+
+ def device_response
+ client.create(challenge: challenge).to_json # rubocop:disable Rails/SaveBang
+ end
+
+ it 'update failed_attempts when proper password is not given' do
+ go
+ expect(user.failed_attempts).to be_eql(1)
+ end
+
+ context "when valid password is given" do
+ it "registers and render OTP backup codes" do
+ post :create_webauthn, params: params_with_password
+ expect(user.otp_backup_codes).not_to be_empty
+ expect(flash[:notice]).to match(/Your WebAuthn device was registered!/)
+ end
+
+ it 'registers and redirects back if user is already having backup codes' do
+ expect(user).to receive(:otp_backup_codes?).and_return(true)
+ post :create_webauthn, params: params_with_password
+ expect(response).to redirect_to(profile_two_factor_auth_path)
+ expect(flash[:notice]).to match(/Your WebAuthn device was registered!/)
+ end
+ end
+
+ context "when the feature flag 'webauthn_without_totp' is disabled" do
+ before do
+ stub_feature_flags(webauthn_without_totp: false)
+ session[:challenge] = challenge
+ end
+
+ let(:params) { { device_registration: { name: 'touch id', device_response: device_response } } } # rubocop:disable Rails/SaveBang
+
+ it "does not validate the current_password" do
+ go
+
+ expect(flash[:notice]).to match(/Your WebAuthn device was registered!/)
+ expect(response).to redirect_to(profile_two_factor_auth_path)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
def go
delete :destroy, params: { current_password: current_password }
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index aa92ff6be33..daf0f36c28b 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -126,6 +126,16 @@ RSpec.describe ProfilesController, :request_store do
expect(user.reload.pronunciation).to eq(pronunciation)
expect(response).to have_gitlab_http_status(:found)
end
+
+ it 'allows updating user specified Discord User ID', :aggregate_failures do
+ discord_user_id = '1234567890123456789'
+ sign_in(user)
+
+ put :update, params: { user: { discord: discord_user_id } }
+
+ expect(user.reload.discord).to eq(discord_user_id)
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
describe 'GET audit_log' do
diff --git a/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
index 5b434eb2011..a3a86138f18 100644
--- a/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
+++ b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb
@@ -30,6 +30,20 @@ RSpec.describe Projects::Analytics::CycleAnalytics::ValueStreamsController do
expect(json_response.first['name']).to eq('default')
end
+
+ # testing the authorize method within ValueStreamActions
+ context 'when issues and merge requests are disabled' do
+ it 'renders 404' do
+ project.project_feature.update!(
+ issues_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED
+ )
+
+ get :index, params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
context 'when user is not member of the project' do
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 3d12926c07a..32cd10d9805 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ArtifactsController do
+RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts do
include RepoHelpers
let(:user) { project.first_owner }
@@ -37,23 +37,6 @@ RSpec.describe Projects::ArtifactsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('projects/artifacts/index')
-
- app = Nokogiri::HTML.parse(response.body).at_css('div#js-artifact-management')
-
- expect(app.attributes['data-project-path'].value).to eq(project.full_path)
- expect(app.attributes['data-can-destroy-artifacts'].value).to eq('true')
- end
-
- describe 'when user does not have permission to delete artifacts' do
- let(:user) { create(:user) }
-
- it 'passes false to the artifacts app' do
- subject
-
- app = Nokogiri::HTML.parse(response.body).at_css('div#js-artifact-management')
-
- expect(app.attributes['data-can-destroy-artifacts'].value).to eq('false')
- end
end
end
@@ -127,7 +110,7 @@ RSpec.describe Projects::ArtifactsController do
end
context 'when no file type is supplied' do
- let(:filename) { job.artifacts_file.filename }
+ let(:filename) { job.job_artifacts_archive.filename }
it 'sends the artifacts file' do
expect(controller).to receive(:send_file)
@@ -141,6 +124,38 @@ RSpec.describe Projects::ArtifactsController do
end
end
+ context 'when artifact is set as private' do
+ let(:filename) { job.artifacts_file.filename }
+
+ before do
+ job.job_artifacts.update_all(accessibility: 'private')
+ end
+
+ context 'and user is not authoirized' do
+ let(:user) { create(:user) }
+
+ it 'returns forbidden' do
+ download_artifact(file_type: 'archive')
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'and user has access to project' do
+ it 'downloads' do
+ expect(controller).to receive(:send_file)
+ .with(
+ job.artifacts_file.file.path,
+ hash_including(disposition: 'attachment', filename: filename)).and_call_original
+
+ download_artifact(file_type: 'archive')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Disposition']).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
+ end
+ end
+ end
+
context 'when a file type is supplied' do
context 'when an invalid file type is supplied' do
let(:file_type) { 'invalid' }
@@ -255,10 +270,14 @@ RSpec.describe Projects::ArtifactsController do
end
context 'when the user has update_build permissions' do
+ let(:filename) { job.job_artifacts_trace.file.filename }
+
it 'sends the trace' do
download_artifact(file_type: file_type)
expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Disposition'])
+ .to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
end
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index 7077aae6b45..70178083e71 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -4,10 +4,14 @@ require 'spec_helper'
RSpec.describe Projects::AutocompleteSourcesController do
let_it_be(:group, reload: true) { create(:group) }
+ let_it_be(:private_group) { create(:group, :private) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:public_project) { create(:project, :public, group: group) }
let_it_be(:development) { create(:label, project: project, name: 'Development') }
- let_it_be(:issue) { create(:labeled_issue, project: project, labels: [development]) }
+ let_it_be(:private_issue) { create(:labeled_issue, project: project, labels: [development]) }
+ let_it_be(:private_work_item) { create(:work_item, project: project) }
+ let_it_be(:issue) { create(:labeled_issue, project: public_project, labels: [development]) }
+ let_it_be(:work_item) { create(:work_item, project: public_project, id: 1, iid: 100) }
let_it_be(:user) { create(:user) }
def members_by_username(username)
@@ -22,7 +26,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
context 'with a public project' do
shared_examples 'issuable commands' do
it 'returns empty array when no user logged in' do
- get :commands, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type, type_id: issuable_iid }
+ get :commands, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
@@ -52,6 +56,13 @@ RSpec.describe Projects::AutocompleteSourcesController do
it_behaves_like 'issuable commands'
end
+ context 'with work items' do
+ let(:issuable_type) { work_item.class.name }
+ let(:issuable_iid) { work_item.iid }
+
+ it_behaves_like 'issuable commands'
+ end
+
context 'with merge request' do
let(:merge_request) { create(:merge_request, target_project: public_project, source_project: public_project) }
let(:issuable_type) { merge_request.class.name }
@@ -68,24 +79,49 @@ RSpec.describe Projects::AutocompleteSourcesController do
sign_in(user)
end
- it 'raises an error when no target type specified' do
- expect { get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path } }
- .to raise_error(ActionController::ParameterMissing)
+ shared_examples 'label commands' do
+ it 'raises an error when no target type specified' do
+ expect { get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path } }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+
+ it 'returns an array of labels' do
+ get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issuable_type }
+
+ expect(json_response).to be_a(Array)
+ expect(json_response.count).to eq(1)
+ expect(json_response[0]['title']).to eq('Development')
+ end
+ end
+
+ context 'with issues' do
+ let(:issuable_type) { issue.class.name }
+ let(:issuable_iid) { issue.iid }
+
+ it_behaves_like 'label commands'
end
- it 'returns an array of labels' do
- get :labels, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ context 'with work items' do
+ let(:issuable_type) { work_item.class.name }
+ let(:issuable_iid) { work_item.iid }
- expect(json_response).to be_a(Array)
- expect(json_response.count).to eq(1)
- expect(json_response[0]['title']).to eq('Development')
+ it_behaves_like 'label commands'
end
end
describe 'GET members' do
+ let_it_be(:invited_private_member) { create(:user) }
+ let_it_be(:issue) { create(:labeled_issue, project: public_project, labels: [development], author: user) }
+ let_it_be(:work_item) { create(:work_item, project: public_project, author: user) }
+
+ before_all do
+ create(:project_group_link, group: private_group, project: public_project)
+ group.add_owner(user)
+ private_group.add_developer(invited_private_member)
+ end
+
context 'when logged in' do
before do
- group.add_owner(user)
sign_in(user)
end
@@ -94,42 +130,88 @@ RSpec.describe Projects::AutocompleteSourcesController do
.to raise_error(ActionController::ParameterMissing)
end
- it 'returns an array of member object' do
- get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ shared_examples 'all members are returned' do
+ it 'returns an array of member object' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
+
+ expect(members_by_username('all').symbolize_keys).to include(
+ username: 'all',
+ name: 'All Project and Group Members',
+ count: 2)
+
+ expect(members_by_username(group.full_path).symbolize_keys).to include(
+ type: group.class.name,
+ name: group.full_name,
+ avatar_url: group.avatar_url,
+ count: 1)
+
+ expect(members_by_username(user.username).symbolize_keys).to include(
+ type: user.class.name,
+ name: user.name,
+ avatar_url: user.avatar_url)
+
+ expect(members_by_username(invited_private_member.username).symbolize_keys).to include(
+ type: invited_private_member.class.name,
+ name: invited_private_member.name,
+ avatar_url: invited_private_member.avatar_url)
+ end
+ end
- expect(members_by_username('all').symbolize_keys).to include(
- username: 'all',
- name: 'All Project and Group Members',
- count: 1)
+ context 'with issue' do
+ let(:issuable_type) { issue.class.name }
- expect(members_by_username(group.full_path).symbolize_keys).to include(
- type: group.class.name,
- name: group.full_name,
- avatar_url: group.avatar_url,
- count: 1)
+ it_behaves_like 'all members are returned'
+ end
- expect(members_by_username(user.username).symbolize_keys).to include(
- type: user.class.name,
- name: user.name,
- avatar_url: user.avatar_url)
+ context 'with work item' do
+ let(:issuable_type) { work_item.class.name }
+
+ it_behaves_like 'all members are returned'
end
end
context 'when anonymous' do
- it 'redirects to login page' do
- get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ shared_examples 'private project is inaccessible' do
+ it 'redirects to login page for private project' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issuable_type }
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to new_user_session_path
+ end
end
- context 'with public project' do
- it 'returns no members' do
- get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issue.class.name, type_id: issue.id }
+ shared_examples 'only public members are returned for public project' do
+ it 'only returns public members' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: public_project.path, type: issuable_type }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a(Array)
- expect(json_response.count).to eq(1)
- expect(json_response.first['count']).to eq(0)
+ expect(members_by_username('all').symbolize_keys).to include(
+ username: 'all',
+ name: 'All Project and Group Members',
+ count: 1)
+
+ expect(members_by_username(user.username).symbolize_keys).to include(
+ type: user.class.name,
+ name: user.name,
+ avatar_url: user.avatar_url)
+ end
+ end
+
+ context 'with issue' do
+ it_behaves_like 'private project is inaccessible' do
+ let(:issuable_type) { private_issue.class.name }
+ end
+
+ it_behaves_like 'only public members are returned for public project' do
+ let(:issuable_type) { issue.class.name }
+ end
+ end
+
+ context 'with work item' do
+ it_behaves_like 'private project is inaccessible' do
+ let(:issuable_type) { private_work_item.class.name }
+ end
+
+ it_behaves_like 'only public members are returned for public project' do
+ let(:issuable_type) { work_item.class.name }
end
end
end
@@ -184,7 +266,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
it 'lists contacts' do
group.add_developer(user)
- get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name }
emails = json_response.map { |contact_data| contact_data["email"] }
expect(emails).to match_array([contact_1.email, contact_2.email])
@@ -193,7 +275,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
context 'when a user can not read contacts' do
it 'renders 404' do
- get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -204,7 +286,7 @@ RSpec.describe Projects::AutocompleteSourcesController do
it 'renders 404' do
group.add_developer(user)
- get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+ get :contacts, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index ed11d5936b0..dcde22c1fd6 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::BranchesController do
+RSpec.describe Projects::BranchesController, feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:developer) { create(:user) }
@@ -676,6 +676,18 @@ RSpec.describe Projects::BranchesController do
end
end
+ context 'when state is not supported' do
+ before do
+ get :index, format: :html, params: {
+ namespace_id: project.namespace, project_id: project, state: 'unknown'
+ }
+ end
+
+ it 'returns 404 page' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'sorting', :aggregate_failures do
let(:sort) { 'name_asc' }
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 894f0f8354d..a4f7c92f5cd 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ClustersController do
+RSpec.describe Projects::ClustersController, feature_category: :kubernetes_management do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
include KubernetesHelpers
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index edb07bbdce6..8d3939d8133 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -36,6 +36,7 @@ RSpec.describe Projects::CommitController do
go(id: commit.id)
expect(response).to be_ok
+ expect(assigns(:ref)).to eq commit.id
end
context 'when a pipeline job is running' do
@@ -57,6 +58,7 @@ RSpec.describe Projects::CommitController do
go(id: commit.id.reverse)
expect(response).to be_not_found
+ expect(assigns(:ref)).to be_nil
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 26d4725656f..67aa82dacbb 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/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
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index dddefbac163..169fed1ab17 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/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
include MetricsDashboardHelpers
include KubernetesHelpers
@@ -103,6 +103,24 @@ RSpec.describe Projects::EnvironmentsController do
expect(json_response['stopped_count']).to eq 1
end
+ context 'can access stop stale environments feature' do
+ it 'maintainers can access the feature' do
+ get :index, params: environment_params(format: :json)
+
+ expect(json_response['can_stop_stale_environments']).to be_truthy
+ end
+
+ context 'when user is a reporter' do
+ let(:user) { reporter }
+
+ it 'reporters cannot access the feature' do
+ get :index, params: environment_params(format: :json)
+
+ expect(json_response['can_stop_stale_environments']).to be_falsey
+ end
+ end
+ end
+
context 'when enable_environments_search_within_folder FF is disabled' do
before do
stub_feature_flags(enable_environments_search_within_folder: false)
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 962ef93dc72..25c722173c1 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ForksController do
+RSpec.describe Projects::ForksController, feature_category: :source_code_management do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user, name: 'Some name').execute }
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 31e297e5773..9c272872a73 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::IssuesController do
+RSpec.describe Projects::IssuesController, feature_category: :team_planning do
include ProjectForksHelper
include_context 'includes Spam constants'
@@ -647,9 +647,8 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when allow_possible_spam feature flag is false' do
+ context 'when allow_possible_spam application setting is false' do
before do
- stub_feature_flags(allow_possible_spam: false)
expect(controller).to(receive(:spam_action_response_fields).with(issue)) do
spam_action_response_fields
end
@@ -662,7 +661,11 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it 'updates the issue' do
subject
@@ -887,11 +890,7 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
+ context 'when allow_possible_spam application setting is false' do
it 'rejects an issue recognized as spam' do
expect { update_issue }.not_to change { issue.reload.title }
end
@@ -925,7 +924,11 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it 'updates the issue recognized as spam' do
expect { update_issue }.to change { issue.reload.title }
end
@@ -1234,8 +1237,6 @@ RSpec.describe Projects::IssuesController do
context 'when SpamVerdictService allows the issue' do
before do
- stub_feature_flags(allow_possible_spam: false)
-
expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
expect(verdict_service).to receive(:execute).and_return(ALLOW)
end
@@ -1258,11 +1259,7 @@ RSpec.describe Projects::IssuesController do
post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
+ context 'when allow_possible_spam application setting is false' do
it 'rejects an issue recognized as spam' do
expect { post_spam_issue }.not_to change(Issue, :count)
end
@@ -1283,7 +1280,11 @@ RSpec.describe Projects::IssuesController do
end
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it 'creates an issue recognized as spam' do
expect { post_spam_issue }.to change(Issue, :count)
end
@@ -1377,13 +1378,13 @@ RSpec.describe Projects::IssuesController do
end
context 'when issue creation limits imposed' do
- it 'prevents from creating more issues', :request_store do
- post_new_issue
-
- expect { post_new_issue }
- .to change { Gitlab::GitalyClient.get_request_count }.by(1) # creates 1 projects and 0 issues
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
- post_new_issue
+ it 'prevents from creating more issues', :request_store do
+ 2.times { post_new_issue_in_project }
expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
expect(response).to have_gitlab_http_status(:too_many_requests)
@@ -1402,16 +1403,15 @@ RSpec.describe Projects::IssuesController do
expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
- project.add_developer(user)
- sign_in(user)
+ 2.times { post_new_issue_in_project }
+ end
- 2.times do
- post :create, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- issue: { title: 'Title', description: 'Description' }
- }
- end
+ def post_new_issue_in_project
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ issue: { title: 'Title', description: 'Description' }
+ }
end
end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 3dc89365530..8fb9623c21a 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
+RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, feature_category: :continuous_integration do
include ApiHelpers
include HttpIOHelpers
@@ -809,14 +809,48 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
+ context 'when job is not retryable' do
+ context 'and the job is a bridge' do
+ let(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ post_retry
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'and the job is a build' do
+ let(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }
+
+ it 'renders unprocessable_entity' do
+ post_retry
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+
context 'when job is retryable' do
- let(:job) { create(:ci_build, :retryable, pipeline: pipeline) }
+ context 'and the job is a bridge' do
+ let(:job) { create(:ci_bridge, :retryable, pipeline: pipeline) }
- it 'redirects to the retried job page' do
- post_retry
+ it 'responds :ok' do
+ post_retry
- expect(response).to have_gitlab_http_status(:found)
- expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'and the job is a build' do
+ let(:job) { create(:ci_build, :retryable, pipeline: pipeline) }
+
+ it 'redirects to the retried job page' do
+ post_retry
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
+ end
end
shared_examples_for 'retried job has the same attributes' do
@@ -847,16 +881,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- context 'when job is not retryable' do
- let(:job) { create(:ci_build, pipeline: pipeline) }
-
- it 'renders unprocessable_entity' do
- post_retry
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
- end
-
def post_retry
post :retry, params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index dfa6ed639b6..98982856d6c 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::LabelsController do
+RSpec.describe Projects::LabelsController, feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/controllers/projects/learn_gitlab_controller_spec.rb b/spec/controllers/projects/learn_gitlab_controller_spec.rb
deleted file mode 100644
index a93da82d948..00000000000
--- a/spec/controllers/projects/learn_gitlab_controller_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::LearnGitlabController do
- describe 'GET #index' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, namespace: create(:group)) }
-
- let(:learn_gitlab_enabled) { true }
- let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
-
- subject(:action) { get :index, params: params }
-
- before do
- project.namespace.add_owner(user)
- allow(controller.helpers).to receive(:learn_gitlab_enabled?).and_return(learn_gitlab_enabled)
- end
-
- context 'unauthenticated user' do
- it { is_expected.to have_gitlab_http_status(:redirect) }
- end
-
- context 'authenticated user' do
- before do
- sign_in(user)
- end
-
- it { is_expected.to render_template(:index) }
-
- context 'learn_gitlab experiment not enabled' do
- let(:learn_gitlab_enabled) { false }
-
- it { is_expected.to have_gitlab_http_status(:not_found) }
- end
-
- context 'with invite_for_help_continuous_onboarding experiment' do
- it 'tracks the assignment', :experiment do
- stub_experiments(invite_for_help_continuous_onboarding: true)
-
- expect(experiment(:invite_for_help_continuous_onboarding))
- .to track(:assignment).with_context(namespace: project.namespace).on_next_instance
-
- action
- end
- end
- 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 7db708e0e78..3d4a884587f 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequests::CreationsController do
+RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:fork_project) { create(:forked_project_with_submodules) }
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 4de724fd6d6..23a33d7e0b1 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequests::DiffsController do
+RSpec.describe Projects::MergeRequests::DiffsController, feature_category: :code_review_workflow do
include ProjectForksHelper
include TrackingHelpers
@@ -610,5 +610,39 @@ RSpec.describe Projects::MergeRequests::DiffsController do
go
end
end
+
+ context 'when ck param is present' do
+ let(:cache_key) { merge_request.merge_head_diff.id }
+
+ before do
+ create(:merge_request_diff, :merge_head, merge_request: merge_request)
+ end
+
+ it 'sets Cache-Control with max-age' do
+ go(ck: cache_key, diff_head: true)
+
+ expect(response.headers['Cache-Control']).to eq('max-age=86400, private')
+ end
+
+ context 'when diffs_batch_cache_with_max_age feature flag is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_cache_with_max_age: false)
+ end
+
+ it 'does not set Cache-Control with max-age' do
+ go(ck: cache_key, diff_head: true)
+
+ expect(response.headers['Cache-Control']).not_to eq('max-age=86400, private')
+ end
+ end
+
+ context 'when not rendering merge head diff' do
+ it 'does not set Cache-Control with max-age' do
+ go(ck: cache_key, diff_head: false)
+
+ expect(response.headers['Cache-Control']).not_to eq('max-age=86400, private')
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 182d654aaa8..39482938a8b 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -388,104 +388,72 @@ RSpec.describe Projects::MergeRequests::DraftsController do
context 'publish with note' do
before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_submit_review_comment)
+
create(:draft_note, merge_request: merge_request, author: user)
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(mr_review_submit_comment: false)
- end
-
- it 'does not create note' do
- post :publish, params: params.merge!(note: 'Hello world')
+ it 'creates note' do
+ post :publish, params: params.merge!(note: 'Hello world')
- expect(merge_request.notes.reload.size).to be(1)
- end
+ expect(merge_request.notes.reload.size).to be(2)
end
- context 'when feature flag is enabled' do
- before do
- allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
- .to receive(:track_submit_review_comment)
- end
-
- it 'creates note' do
- post :publish, params: params.merge!(note: 'Hello world')
-
- expect(merge_request.notes.reload.size).to be(2)
- end
+ it 'does not create note when note param is empty' do
+ post :publish, params: params.merge!(note: '')
- it 'does not create note when note param is empty' do
- post :publish, params: params.merge!(note: '')
-
- expect(merge_request.notes.reload.size).to be(1)
- end
+ expect(merge_request.notes.reload.size).to be(1)
+ end
- it 'tracks merge request activity' do
- post :publish, params: params.merge!(note: 'Hello world')
+ it 'tracks merge request activity' do
+ post :publish, params: params.merge!(note: 'Hello world')
- expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
- .to have_received(:track_submit_review_comment).with(user: user)
- end
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_submit_review_comment).with(user: user)
end
end
context 'approve merge request' do
before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_submit_review_approve)
+
create(:draft_note, merge_request: merge_request, author: user)
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(mr_review_submit_comment: false)
- end
+ it 'approves merge request' do
+ post :publish, params: params.merge!(approve: true)
- it 'does not approve' do
- post :publish, params: params.merge!(approve: true)
-
- expect(merge_request.approvals.reload.size).to be(0)
- end
+ expect(merge_request.approvals.reload.size).to be(1)
end
- context 'when feature flag is enabled' do
- before do
- allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
- .to receive(:track_submit_review_approve)
- end
+ it 'does not approve merge request' do
+ post :publish, params: params.merge!(approve: false)
- it 'approves merge request' do
- post :publish, params: params.merge!(approve: true)
+ expect(merge_request.approvals.reload.size).to be(0)
+ end
- expect(merge_request.approvals.reload.size).to be(1)
- end
+ it 'tracks merge request activity' do
+ post :publish, params: params.merge!(approve: true)
- it 'does not approve merge request' do
- post :publish, params: params.merge!(approve: false)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_submit_review_approve).with(user: user)
+ end
- expect(merge_request.approvals.reload.size).to be(0)
+ context 'when merge request is already approved by user' do
+ before do
+ create(:approval, merge_request: merge_request, user: user)
end
- it 'tracks merge request activity' do
+ it 'does return 200' do
post :publish, params: params.merge!(approve: true)
+ expect(response).to have_gitlab_http_status(:ok)
+
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to have_received(:track_submit_review_approve).with(user: user)
end
-
- context 'when merge request is already approved by user' do
- before do
- create(:approval, merge_request: merge_request, user: user)
- end
-
- it 'does return 200' do
- post :publish, params: params.merge!(approve: true)
-
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
- .to have_received(:track_submit_review_approve).with(user: user)
- end
- end
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 095775b0ddd..ceb3f803db5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -68,6 +68,72 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
end
+ context 'when add_prepared_state_to_mr feature flag on' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: true)
+ end
+
+ context 'when the merge request is not prepared' do
+ before do
+ merge_request.update!(prepared_at: nil, created_at: 10.minutes.ago)
+ end
+
+ it 'prepares the merge request' do
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+
+ go
+ end
+
+ context 'when the merge request was created less than 5 minutes ago' do
+ it 'does not prepare the merge request again' do
+ travel_to(4.minutes.from_now) do
+ merge_request.update!(created_at: Time.current - 4.minutes)
+
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+
+ context 'when the merge request was created 5 minutes ago' do
+ it 'prepares the merge request' do
+ travel_to(6.minutes.from_now) do
+ merge_request.update!(created_at: Time.current - 6.minutes)
+
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+ end
+
+ context 'when the merge request is prepared' do
+ before do
+ merge_request.update!(prepared_at: Time.current, created_at: 10.minutes.ago)
+ end
+
+ it 'prepares the merge request' do
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is off' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: false)
+ end
+
+ it 'does not prepare the merge request again' do
+ expect(NewMergeRequestWorker).not_to receive(:perform_async)
+
+ go
+ end
+ end
+
describe 'as html' do
it 'sets the endpoint_metadata_url' do
go
@@ -82,6 +148,61 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
w: '0'))
end
+ context 'when merge_head diff is present' do
+ before do
+ create(:merge_request_diff, :merge_head, merge_request: merge_request)
+ end
+
+ it 'sets the endpoint_diff_batch_url with ck' do
+ go
+
+ expect(assigns["endpoint_diff_batch_url"]).to eq(
+ diffs_batch_project_json_merge_request_path(
+ project,
+ merge_request,
+ 'json',
+ diff_head: true,
+ view: 'inline',
+ w: '0',
+ page: '0',
+ per_page: '5',
+ ck: merge_request.merge_head_diff.id))
+ end
+
+ it 'sets diffs_batch_cache_key' do
+ go
+
+ expect(assigns['diffs_batch_cache_key']).to eq(merge_request.merge_head_diff.id)
+ end
+
+ context 'when diffs_batch_cache_with_max_age feature flag is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_cache_with_max_age: false)
+ end
+
+ it 'sets the endpoint_diff_batch_url without ck param' do
+ go
+
+ expect(assigns['endpoint_diff_batch_url']).to eq(
+ diffs_batch_project_json_merge_request_path(
+ project,
+ merge_request,
+ 'json',
+ diff_head: true,
+ view: 'inline',
+ w: '0',
+ page: '0',
+ per_page: '5'))
+ end
+
+ it 'does not set diffs_batch_cache_key' do
+ go
+
+ expect(assigns['diffs_batch_cache_key']).to be_nil
+ end
+ end
+ end
+
context 'when diff files were cleaned' do
render_views
@@ -335,15 +456,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
end
end
end
-
- it_behaves_like 'issuable list with anonymous search disabled' do
- let(:params) { { namespace_id: project.namespace, project_id: project } }
-
- before do
- sign_out(user)
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
- end
end
describe 'PUT update' do
@@ -1856,18 +1968,26 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review
}
end
- it 'shows a flash message on success' do
+ it 'displays an flash error message on fail' do
+ allow(MergeRequests::AssignIssuesService).to receive(:new).and_return(double(execute: { count: 0 }))
+
post_assign_issues
- expect(flash[:notice]).to eq '2 issues have been assigned to you'
+ expect(flash[:alert]).to eq _('Failed to assign you issues related to the merge request.')
end
- it 'correctly pluralizes flash message on success' do
+ it 'shows a flash message on success' do
issue2.assignees = [user]
post_assign_issues
- expect(flash[:notice]).to eq '1 issue has been assigned to you'
+ expect(flash[:notice]).to eq n_("An issue has been assigned to you.", "%d issues have been assigned to you.", 1)
+ end
+
+ it 'correctly pluralizes flash message on success' do
+ post_assign_issues
+
+ expect(flash[:notice]).to eq n_("An issue has been assigned to you.", "%d issues have been assigned to you.", 2)
end
it 'calls MergeRequests::AssignIssuesService' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 0afd2e10ea2..23b0b58158f 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::NotesController do
+RSpec.describe Projects::NotesController, type: :controller, feature_category: :team_planning do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 358b98621a5..a628c1ab230 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PipelineSchedulesController do
+RSpec.describe Projects::PipelineSchedulesController, feature_category: :continuous_integration do
include AccessMatchersForController
let_it_be(:user) { create(:user) }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 3d1d28945f7..4e0c098ad81 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/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
include ApiHelpers
let_it_be(:user) { create(:user) }
@@ -52,21 +52,6 @@ RSpec.describe Projects::PipelinesController do
expect(stages.count).to eq 3
end
end
-
- it 'does not execute N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/345470' do
- get_pipelines_index_json
-
- control_count = ActiveRecord::QueryRecorder.new do
- get_pipelines_index_json
- end.count
-
- create_all_pipeline_types
-
- # There appears to be one extra query for Pipelines#has_warnings? for some reason
- expect { get_pipelines_index_json }.not_to exceed_query_limit(control_count + 1)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['pipelines'].count).to eq 12
- end
end
it 'does not include coverage data for the pipelines' do
@@ -83,14 +68,7 @@ RSpec.describe Projects::PipelinesController do
check_pipeline_response(returned: 2, all: 6)
end
- context 'when performing gitaly calls', :request_store do
- before do
- # To prevent double writes / fallback read due to MultiStore which is failing the `Gitlab::GitalyClient
- # .get_request_count` expectation.
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- end
-
+ context 'when performing gitaly calls', :request_store, :use_null_store_as_repository_cache do
it 'limits the Gitaly requests' do
# Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end!
@@ -1355,8 +1333,8 @@ RSpec.describe Projects::PipelinesController do
.and_return(service)
end
- context 'when sending a valid sha' do
- let(:sha) { 'master' }
+ context 'when sending a valid ref' do
+ let(:ref) { 'master' }
let(:ci_config) do
{
variables: {
@@ -1381,8 +1359,8 @@ RSpec.describe Projects::PipelinesController do
end
end
- context 'when sending an invalid sha' do
- let(:sha) { 'invalid-sha' }
+ context 'when sending an invalid ref' do
+ let(:ref) { 'invalid-ref' }
before do
synchronous_reactive_cache(service)
@@ -1397,7 +1375,7 @@ RSpec.describe Projects::PipelinesController do
end
context 'when sending an invalid config' do
- let(:sha) { 'master' }
+ let(:ref) { 'master' }
let(:ci_config) do
{
variables: {
@@ -1423,7 +1401,7 @@ RSpec.describe Projects::PipelinesController do
end
context 'when the cache is empty' do
- let(:sha) { 'master' }
+ let(:ref) { 'master' }
let(:ci_config) do
{
variables: {
@@ -1446,7 +1424,7 @@ RSpec.describe Projects::PipelinesController do
context 'when project uses external project ci config' do
let(:other_project) { create(:project, :custom_repo, files: other_project_files) }
let(:other_project_files) { { '.gitlab-ci.yml' => YAML.dump(other_project_ci_config) } }
- let(:sha) { 'master' }
+ let(:ref) { 'master' }
let(:other_project_ci_config) do
{
@@ -1479,7 +1457,7 @@ RSpec.describe Projects::PipelinesController do
def get_config_variables
get :config_variables, params: { namespace_id: project.namespace,
project_id: project,
- sha: sha },
+ sha: ref },
format: :json
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index fb27fe58cd9..ab33195eb83 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -8,183 +8,168 @@ RSpec.describe Projects::ProjectMembersController do
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:project, reload: true) { create(:project, :public) }
- before do
- travel_to DateTime.new(2019, 4, 1)
- end
+ shared_examples_for 'controller actions' do
+ before do
+ travel_to DateTime.new(2019, 4, 1)
+ end
- after do
- travel_back
- end
+ after do
+ travel_back
+ end
- describe 'GET index' do
- it 'has the project_members address with a 200 status code' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ describe 'GET index' do
+ it 'has the project_members address with a 200 status code' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(response).to have_gitlab_http_status(:ok)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ end
- context 'project members' do
- context 'when project belongs to group' do
- let_it_be(:user_in_group) { create(:user) }
- let_it_be(:project_in_group) { create(:project, :public, group: group) }
+ context 'project members' do
+ context 'when project belongs to group' do
+ let_it_be(:user_in_group) { create(:user) }
+ let_it_be(:project_in_group) { create(:project, :public, group: group) }
- before do
- group.add_owner(user_in_group)
- project_in_group.add_maintainer(user)
- sign_in(user)
- end
+ before do
+ group.add_owner(user_in_group)
+ project_in_group.add_maintainer(user)
+ sign_in(user)
+ end
- it 'lists inherited project members by default' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
+ it 'lists inherited project members by default' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
- end
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
+ end
- it 'lists direct project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
+ it 'lists direct project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
- end
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
+ end
- it 'lists inherited project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
+ it 'lists inherited project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ end
end
- end
- context 'when project belongs to a sub-group' do
- let_it_be(:user_in_group) { create(:user) }
- let_it_be(:project_in_group) { create(:project, :public, group: sub_group) }
+ context 'when project belongs to a sub-group' do
+ let_it_be(:user_in_group) { create(:user) }
+ let_it_be(:project_in_group) { create(:project, :public, group: sub_group) }
- before do
- group.add_owner(user_in_group)
- project_in_group.add_maintainer(user)
- sign_in(user)
- end
+ before do
+ group.add_owner(user_in_group)
+ project_in_group.add_maintainer(user)
+ sign_in(user)
+ end
- it 'lists inherited project members by default' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
+ it 'lists inherited project members by default' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
- end
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id)
+ end
- it 'lists direct project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
+ it 'lists direct project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
- end
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id)
+ end
- it 'lists inherited project members only' do
- get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
+ it 'lists inherited project members only' do
+ get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' }
- expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id)
+ end
end
- end
- context 'when invited project members are present' do
- let!(:invited_member) { create(:project_member, :invited, project: project) }
+ context 'when invited project members are present' do
+ let!(:invited_member) { create(:project_member, :invited, project: project) }
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
- it 'excludes the invited members from project members list' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ it 'excludes the invited members from project members list' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email)
+ expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email)
+ end
end
end
- end
-
- context 'invited members' do
- let_it_be(:invited_member) { create(:project_member, :invited, project: project) }
- before do
- sign_in(user)
- end
+ context 'invited members' do
+ let_it_be(:invited_member) { create(:project_member, :invited, project: project) }
- context 'when user has `admin_project_member` permissions' do
before do
- project.add_maintainer(user)
+ sign_in(user)
end
- it 'lists invited members' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ context 'when user has `admin_project_member` permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'lists invited members' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email)
+ expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email)
+ end
end
- end
- context 'when user does not have `admin_project_member` permissions' do
- it 'does not list invited members' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ context 'when user does not have `admin_project_member` permissions' do
+ it 'does not list invited members' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:invited_members)).to be_nil
+ expect(assigns(:invited_members)).to be_nil
+ end
end
end
- end
- context 'access requests' do
- let_it_be(:access_requester_user) { create(:user) }
-
- before do
- project.request_access(access_requester_user)
- sign_in(user)
- end
+ context 'access requests' do
+ let_it_be(:access_requester_user) { create(:user) }
- context 'when user has `admin_project_member` permissions' do
before do
- project.add_maintainer(user)
+ project.request_access(access_requester_user)
+ sign_in(user)
end
- it 'lists access requests' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ context 'when user has `admin_project_member` permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'lists access requests' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id)
+ expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id)
+ end
end
- end
- context 'when user does not have `admin_project_member` permissions' do
- it 'does not list access requests' do
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ context 'when user does not have `admin_project_member` permissions' do
+ it 'does not list access requests' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
- expect(assigns(:requesters)).to be_nil
+ expect(assigns(:requesters)).to be_nil
+ end
end
end
end
- end
-
- describe 'PUT update' do
- let_it_be(:requester) { create(:project_member, :access_request, project: project) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
- context 'access level' do
- Gitlab::Access.options.each do |label, value|
- it "can change the access level to #{label}" do
- params = {
- project_member: { access_level: value },
- namespace_id: project.namespace,
- project_id: project,
- id: requester
- }
+ describe 'PUT update' do
+ let_it_be(:requester) { create(:project_member, :access_request, project: project) }
- put :update, params: params, xhr: true
-
- expect(requester.reload.human_access).to eq(label)
- end
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
end
- describe 'managing project direct owners' do
- context 'when a Maintainer tries to elevate another user to OWNER' do
- it 'does not allow the operation' do
+ context 'access level' do
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
params = {
- project_member: { access_level: Gitlab::Access::OWNER },
+ project_member: { access_level: value },
namespace_id: project.namespace,
project_id: project,
id: requester
@@ -192,368 +177,395 @@ RSpec.describe Projects::ProjectMembersController do
put :update, params: params, xhr: true
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(requester.reload.human_access).to eq(label)
end
end
- context 'when a user with OWNER access tries to elevate another user to OWNER' do
- # inherited owner role via personal project association
- let(:user) { project.first_owner }
+ describe 'managing project direct owners' do
+ context 'when a Maintainer tries to elevate another user to OWNER' do
+ it 'does not allow the operation' do
+ params = {
+ project_member: { access_level: Gitlab::Access::OWNER },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+ }
- before do
- sign_in(user)
+ put :update, params: params, xhr: true
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
- it 'returns success' do
- params = {
- project_member: { access_level: Gitlab::Access::OWNER },
- namespace_id: project.namespace,
- project_id: project,
- id: requester
- }
+ context 'when a user with OWNER access tries to elevate another user to OWNER' do
+ # inherited owner role via personal project association
+ let(:user) { project.first_owner }
- put :update, params: params, xhr: true
+ before do
+ sign_in(user)
+ end
+
+ it 'returns success' do
+ params = {
+ project_member: { access_level: Gitlab::Access::OWNER },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+ }
+
+ put :update, params: params, xhr: true
- expect(response).to have_gitlab_http_status(:ok)
- expect(requester.reload.access_level).to eq(Gitlab::Access::OWNER)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(requester.reload.access_level).to eq(Gitlab::Access::OWNER)
+ end
end
end
end
- end
- context 'access expiry date' do
- subject do
- put :update, xhr: true, params: {
- project_member: {
- expires_at: expires_at
- },
- namespace_id: project.namespace,
- project_id: project,
- id: requester
- }
- end
+ context 'access expiry date' do
+ subject do
+ put :update, xhr: true, params: {
+ project_member: {
+ expires_at: expires_at
+ },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+ }
+ end
- context 'when set to a date in the past' do
- let(:expires_at) { 2.days.ago }
+ context 'when set to a date in the past' do
+ let(:expires_at) { 2.days.ago }
- it 'does not update the member' do
- subject
+ it 'does not update the member' do
+ subject
- expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
- end
+ expect(requester.reload.expires_at).not_to eq(expires_at.to_date)
+ end
- it 'returns error status' do
- subject
+ it 'returns error status' do
+ subject
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- end
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
- it 'returns error message' do
- subject
+ it 'returns error message' do
+ subject
- expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' })
+ expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' })
+ end
end
- end
- context 'when set to a date in the future' do
- let(:expires_at) { 5.days.from_now }
+ context 'when set to a date in the future' do
+ let(:expires_at) { 5.days.from_now }
- it 'updates the member' do
- subject
+ it 'updates the member' do
+ subject
- expect(requester.reload.expires_at).to eq(expires_at.to_date)
+ expect(requester.reload.expires_at).to eq(expires_at.to_date)
+ end
end
end
- end
- context 'expiration date' do
- let(:expiry_date) { 1.month.from_now.to_date }
+ context 'expiration date' do
+ let(:expiry_date) { 1.month.from_now.to_date }
- before do
- travel_to Time.now.utc.beginning_of_day
-
- put(
- :update,
- params: {
- project_member: { expires_at: expiry_date },
- namespace_id: project.namespace,
- project_id: project,
- id: requester
- },
- format: :json
- )
- end
+ before do
+ travel_to Time.now.utc.beginning_of_day
+
+ put(
+ :update,
+ params: {
+ project_member: { expires_at: expiry_date },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+ },
+ format: :json
+ )
+ end
- context 'when `expires_at` is set' do
- it 'returns correct json response' do
- expect(json_response).to eq({
- "expires_soon" => false,
- "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
- })
+ context 'when `expires_at` is set' do
+ it 'returns correct json response' do
+ expect(json_response).to eq({
+ "expires_soon" => false,
+ "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium)
+ })
+ end
end
- end
- context 'when `expires_at` is not set' do
- let(:expiry_date) { nil }
+ context 'when `expires_at` is not set' do
+ let(:expiry_date) { nil }
- it 'returns empty json response' do
- expect(json_response).to be_empty
+ it 'returns empty json response' do
+ expect(json_response).to be_empty
+ end
end
end
end
- end
- describe 'DELETE destroy' do
- let_it_be(:member) { create(:project_member, :developer, project: project) }
+ describe 'DELETE destroy' do
+ let_it_be(:member) { create(:project_member, :developer, project: project) }
- before do
- sign_in(user)
- end
+ before do
+ sign_in(user)
+ end
- context 'when member is not found' do
- it 'returns 404' do
- delete :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: 42
- }
+ context 'when member is not found' do
+ it 'returns 404' do
+ delete :destroy, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+ }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
- end
- context 'when member is found' do
- context 'when user does not have enough rights' do
- context 'when user does not have rights to manage other members' do
- before do
- project.add_developer(user)
+ context 'when member is found' do
+ context 'when user does not have enough rights' do
+ context 'when user does not have rights to manage other members' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns 404', :aggregate_failures do
+ delete :destroy, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: member
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(project.members).to include member
+ end
end
- it 'returns 404', :aggregate_failures do
- delete :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: member
- }
+ context 'when user does not have rights to manage Owner members' do
+ let_it_be(:member) { create(:project_member, project: project, access_level: Gitlab::Access::OWNER) }
- expect(response).to have_gitlab_http_status(:not_found)
- expect(project.members).to include member
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns 403', :aggregate_failures do
+ delete :destroy, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: member
+ }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(project.members).to include member
+ end
end
end
- context 'when user does not have rights to manage Owner members' do
- let_it_be(:member) { create(:project_member, project: project, access_level: Gitlab::Access::OWNER) }
-
+ context 'when user has enough rights' do
before do
project.add_maintainer(user)
end
- it 'returns 403', :aggregate_failures do
+ it '[HTML] removes user from members', :aggregate_failures do
+ delete :destroy, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: member
+ }
+
+ expect(response).to redirect_to(
+ project_project_members_path(project)
+ )
+ expect(project.members).not_to include member
+ end
+
+ it '[JS] removes user from members', :aggregate_failures do
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
id: member
- }
+ }, xhr: true
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(project.members).to include member
+ expect(response).to be_successful
+ expect(project.members).not_to include member
end
end
end
-
- context 'when user has enough rights' do
- before do
- project.add_maintainer(user)
- end
-
- it '[HTML] removes user from members', :aggregate_failures do
- delete :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: member
- }
-
- expect(response).to redirect_to(
- project_project_members_path(project)
- )
- expect(project.members).not_to include member
- end
-
- it '[JS] removes user from members', :aggregate_failures do
- delete :destroy, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: member
- }, xhr: true
-
- expect(response).to be_successful
- expect(project.members).not_to include member
- end
- end
- end
- end
-
- describe 'DELETE leave' do
- before do
- sign_in(user)
end
- context 'when member is not found' do
- it 'returns 404' do
- delete :leave, params: {
- namespace_id: project.namespace,
- project_id: project
- }
-
- expect(response).to have_gitlab_http_status(:not_found)
+ describe 'DELETE leave' do
+ before do
+ sign_in(user)
end
- end
-
- context 'when member is found' do
- context 'and is not an owner' do
- before do
- project.add_developer(user)
- end
- it 'removes user from members', :aggregate_failures do
+ context 'when member is not found' do
+ it 'returns 404' do
delete :leave, params: {
namespace_id: project.namespace,
project_id: project
}
- expect(controller).to set_flash.to "You left the \"#{project.human_name}\" project."
- expect(response).to redirect_to(dashboard_projects_path)
- expect(project.users).not_to include user
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'and is an owner' do
- let(:project) { create(:project, namespace: user.namespace) }
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'removes user from members', :aggregate_failures do
+ delete :leave, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
- before do
- project.add_maintainer(user)
+ expect(controller).to set_flash.to "You left the \"#{project.human_name}\" project."
+ expect(response).to redirect_to(dashboard_projects_path)
+ expect(project.users).not_to include user
+ end
end
- it 'cannot remove themselves from the project' do
- delete :leave, params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ context 'and is an owner' do
+ let(:project) { create(:project, namespace: user.namespace) }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ before do
+ project.add_maintainer(user)
+ end
- context 'and is a requester' do
- before do
- project.request_access(user)
+ it 'cannot remove themselves from the project' do
+ delete :leave, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
- it 'removes user from members', :aggregate_failures do
- delete :leave, params: {
- namespace_id: project.namespace,
- project_id: project
- }
+ context 'and is a requester' do
+ before do
+ project.request_access(user)
+ end
+
+ it 'removes user from members', :aggregate_failures do
+ delete :leave, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
- expect(controller).to set_flash.to 'Your access request to the project has been withdrawn.'
- expect(response).to redirect_to(project_path(project))
- expect(project.requesters).to be_empty
- expect(project.users).not_to include user
+ expect(controller).to set_flash.to 'Your access request to the project has been withdrawn.'
+ expect(response).to redirect_to(project_path(project))
+ expect(project.requesters).to be_empty
+ expect(project.users).not_to include user
+ end
end
end
end
- end
-
- describe 'POST request_access' do
- before do
- sign_in(user)
- end
- it 'creates a new ProjectMember that is not a team member', :aggregate_failures do
- post :request_access, params: {
- namespace_id: project.namespace,
- project_id: project
- }
-
- expect(controller).to set_flash.to 'Your request for access has been queued for review.'
- expect(response).to redirect_to(
- project_path(project)
- )
- expect(project.requesters.exists?(user_id: user)).to be_truthy
- expect(project.users).not_to include user
- end
- end
+ describe 'POST request_access' do
+ before do
+ sign_in(user)
+ end
- describe 'POST approve' do
- let_it_be(:member) { create(:project_member, :access_request, project: project) }
+ it 'creates a new ProjectMember that is not a team member', :aggregate_failures do
+ post :request_access, params: {
+ namespace_id: project.namespace,
+ project_id: project
+ }
- before do
- sign_in(user)
+ expect(controller).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(
+ project_path(project)
+ )
+ expect(project.requesters.exists?(user_id: user)).to be_truthy
+ expect(project.users).not_to include user
+ end
end
- context 'when member is not found' do
- it 'returns 404' do
- post :approve_access_request, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: 42
- }
+ describe 'POST approve' do
+ let_it_be(:member) { create(:project_member, :access_request, project: project) }
- expect(response).to have_gitlab_http_status(:not_found)
+ before do
+ sign_in(user)
end
- end
-
- context 'when member is found' do
- context 'when user does not have rights to manage other members' do
- before do
- project.add_developer(user)
- end
- it 'returns 404', :aggregate_failures do
+ context 'when member is not found' do
+ it 'returns 404' do
post :approve_access_request, params: {
namespace_id: project.namespace,
project_id: project,
- id: member
+ id: 42
}
expect(response).to have_gitlab_http_status(:not_found)
- expect(project.members).not_to include member
end
end
- context 'when user has enough rights' do
- before do
- project.add_maintainer(user)
+ context 'when member is found' do
+ context 'when user does not have rights to manage other members' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns 404', :aggregate_failures do
+ post :approve_access_request, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: member
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(project.members).not_to include member
+ end
end
- it 'adds user to members', :aggregate_failures do
- post :approve_access_request, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: member
- }
+ context 'when user has enough rights' do
+ before do
+ project.add_maintainer(user)
+ end
- expect(response).to redirect_to(
- project_project_members_path(project)
- )
- expect(project.members).to include member
+ it 'adds user to members', :aggregate_failures do
+ post :approve_access_request, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: member
+ }
+
+ expect(response).to redirect_to(
+ project_project_members_path(project)
+ )
+ expect(project.members).to include member
+ end
end
end
end
- end
- describe 'POST resend_invite' do
- let_it_be(:member) { create(:project_member, project: project) }
+ describe 'POST resend_invite' do
+ let_it_be(:member) { create(:project_member, project: project) }
- before do
- project.add_maintainer(user)
- sign_in(user)
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'is successful' do
+ post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member }
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
+ end
- it 'is successful' do
- post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member }
+ it_behaves_like 'controller actions'
- expect(response).to have_gitlab_http_status(:found)
+ context 'when project_members_index_by_project_namespace feature flag is disabled' do
+ before do
+ stub_feature_flags(project_members_index_by_project_namespace: false)
end
+
+ it_behaves_like 'controller actions'
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 1c9aafacbd9..40252cf65cd 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::RawController do
+RSpec.describe Projects::RawController, feature_category: :source_code_management do
include RepoHelpers
let_it_be(:project) { create(:project, :public, :repository) }
@@ -23,13 +23,13 @@ RSpec.describe Projects::RawController do
subject { get_show }
- shared_examples 'single Gitaly request' do
- it 'makes a single Gitaly request', :request_store, :clean_gitlab_redis_cache do
+ shared_examples 'limited number of Gitaly request' do
+ it 'makes a limited number of Gitaly request', :request_store, :clean_gitlab_redis_cache do
# Warm up to populate repository cache
get_show
RequestStore.clear!
- expect { get_show }.to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ expect { get_show }.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end
end
@@ -57,7 +57,7 @@ RSpec.describe Projects::RawController do
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
- include_examples 'single Gitaly request'
+ include_examples 'limited number of Gitaly request'
end
context 'image header' do
@@ -73,7 +73,7 @@ RSpec.describe Projects::RawController do
it_behaves_like 'project cache control headers'
it_behaves_like 'content disposition headers'
- include_examples 'single Gitaly request'
+ include_examples 'limited number of Gitaly request'
end
context 'with LFS files' do
@@ -82,7 +82,7 @@ RSpec.describe Projects::RawController do
it_behaves_like 'a controller that can serve LFS files'
it_behaves_like 'project cache control headers'
- include_examples 'single Gitaly request'
+ include_examples 'limited number of Gitaly request'
end
context 'when the endpoint receives requests above the limit' do
@@ -239,8 +239,10 @@ RSpec.describe Projects::RawController do
end
describe 'caching' do
+ let(:ref) { project.default_branch }
+
def request_file
- get(:show, params: { namespace_id: project.namespace, project_id: project, id: 'master/README.md' })
+ get(:show, params: { namespace_id: project.namespace, project_id: project, id: "#{ref}/README.md" })
end
it 'sets appropriate caching headers' do
@@ -254,6 +256,21 @@ RSpec.describe Projects::RawController do
)
end
+ context 'when a blob access by permalink' do
+ let(:ref) { project.commit.id }
+
+ it 'sets appropriate caching headers with longer max-age' do
+ sign_in create(:user)
+ request_file
+
+ expect(response.headers['ETag']).to eq("\"bdd5aa537c1e1f6d1b66de4bac8a6132\"")
+ expect(response.cache_control[:no_store]).to be_nil
+ expect(response.header['Cache-Control']).to eq(
+ 'max-age=3600, public, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60'
+ )
+ end
+ end
+
context 'when a public project has private repo' do
let(:project) { create(:project, :public, :repository, :repository_private) }
let(:user) { create(:user, maintainer_projects: [project]) }
@@ -278,21 +295,6 @@ RSpec.describe Projects::RawController do
expect(response).to have_gitlab_http_status(:not_modified)
end
end
-
- context 'when improve_blobs_cache_headers disabled' do
- before do
- stub_feature_flags(improve_blobs_cache_headers: false)
- end
-
- it 'uses weak etags with a restricted set of headers' do
- sign_in create(:user)
- request_file
-
- expect(response.headers['ETag']).to eq("W/\"bdd5aa537c1e1f6d1b66de4bac8a6132\"")
- expect(response.cache_control[:no_store]).to be_nil
- expect(response.header['Cache-Control']).to eq('max-age=60, public')
- end
- end
end
end
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index a7a8361ae20..a0d119baf16 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -22,65 +22,30 @@ RSpec.describe Projects::RefsController, feature_category: :source_code_manageme
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
+ 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
- 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
+ with_them do
+ it 'redirects to destination' do
+ expect(subject).to redirect_to(redirected_to)
end
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 928428b5caf..8186176a46b 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Projects::RepositoriesController do
+RSpec.describe Projects::RepositoriesController, feature_category: :source_code_management do
let_it_be(:project) { create(:project, :repository) }
describe 'POST create' do
@@ -143,7 +143,9 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.header['ETag']).to be_present
- expect(response.header['Cache-Control']).to include('max-age=60, public')
+ expect(response.header['Cache-Control']).to eq(
+ 'max-age=60, public, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60'
+ )
end
context 'and repo is private' do
@@ -154,7 +156,9 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.header['ETag']).to be_present
- expect(response.header['Cache-Control']).to include('max-age=60, private')
+ expect(response.header['Cache-Control']).to eq(
+ 'max-age=60, private, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60'
+ )
end
end
end
@@ -164,7 +168,9 @@ RSpec.describe Projects::RepositoriesController do
get_archive('ddd0f15ae83993f5cb66a927a28673882e99100b')
expect(response).to have_gitlab_http_status(:ok)
- expect(response.header['Cache-Control']).to include('max-age=3600')
+ expect(response.header['Cache-Control']).to eq(
+ 'max-age=3600, private, must-revalidate, stale-while-revalidate=60, stale-if-error=300, s-maxage=60'
+ )
end
end
diff --git a/spec/controllers/projects/service_ping_controller_spec.rb b/spec/controllers/projects/service_ping_controller_spec.rb
index 10d4b897564..601dfd9b011 100644
--- a/spec/controllers/projects/service_ping_controller_spec.rb
+++ b/spec/controllers/projects/service_ping_controller_spec.rb
@@ -42,83 +42,6 @@ RSpec.describe Projects::ServicePingController do
end
end
- describe 'POST #web_ide_clientside_preview' do
- subject { post :web_ide_clientside_preview, params: { namespace_id: project.namespace, project_id: project } }
-
- context 'when web ide clientside preview is enabled' do
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: true)
- end
-
- it_behaves_like 'counter is not increased'
- it_behaves_like 'counter is increased', 'WEB_IDE_PREVIEWS_COUNT'
- end
-
- context 'when web ide clientside preview is not enabled' do
- let(:user) { project.first_owner }
-
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: false)
- end
-
- it 'returns 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'POST #web_ide_clientside_preview_success' do
- subject { post :web_ide_clientside_preview_success, params: { namespace_id: project.namespace, project_id: project } }
-
- context 'when web ide clientside preview is enabled' do
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: true)
- end
-
- it_behaves_like 'counter is not increased'
- it_behaves_like 'counter is increased', 'WEB_IDE_PREVIEWS_SUCCESS_COUNT'
-
- context 'when the user has access to the project', :snowplow do
- let(:user) { project.owner }
-
- it 'increases the live preview view counter' do
- expect(Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_live_preview_edit_action).with(author: user, project: project)
-
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:project) { create(:project) }
- 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
- end
-
- context 'when web ide clientside preview is not enabled' do
- let(:user) { project.owner }
-
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: false)
- end
-
- it 'returns 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
describe 'POST #web_ide_pipelines_count' do
subject { post :web_ide_pipelines_count, params: { namespace_id: project.namespace, project_id: project } }
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index dcd1072612a..ba917fa3a31 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -2,7 +2,7 @@
require('spec_helper')
-RSpec.describe Projects::Settings::CiCdController do
+RSpec.describe Projects::Settings::CiCdController, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project_auto_devops) { create(:project_auto_devops) }
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 51ea2e5d7c6..781e4ff7b00 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -19,40 +19,6 @@ RSpec.describe Projects::Settings::RepositoryController, feature_category: :sour
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
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index bc58eaa1d6f..51f8a3b1197 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -2,7 +2,7 @@
require('spec_helper')
-RSpec.describe ProjectsController do
+RSpec.describe ProjectsController, feature_category: :projects do
include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
@@ -629,29 +629,66 @@ RSpec.describe ProjectsController do
describe '#housekeeping' do
let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
+ let(:housekeeping_service_dbl) { instance_double(Repositories::HousekeepingService) }
+ let(:params) do
+ {
+ namespace_id: project.namespace.path,
+ id: project.path,
+ prune: prune
+ }
+ end
+ let(:prune) { nil }
+ let_it_be(:project) { create(:project, group: group) }
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
+ subject { post :housekeeping, params: params }
+
context 'when authenticated as owner' do
before do
group.add_owner(user)
sign_in(user)
- allow(Repositories::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
+ allow(Repositories::HousekeepingService).to receive(:new).with(project, :eager).and_return(housekeeping)
end
it 'forces a full garbage collection' do
expect(housekeeping).to receive(:execute).once
post :housekeeping,
- params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
expect(response).to have_gitlab_http_status(:found)
end
+
+ it 'logs an audit event' do
+ expect(housekeeping).to receive(:execute).once.and_yield
+
+ expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
+ name: 'manually_trigger_housekeeping',
+ author: user,
+ scope: project,
+ target: project,
+ message: "Housekeeping task: eager"
+ ))
+
+ subject
+ end
+
+ context 'and requesting prune' do
+ let(:prune) { true }
+
+ it 'enqueues pruning' do
+ allow(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping_service_dbl)
+ expect(housekeeping_service_dbl).to receive(:execute)
+
+ subject
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
end
context 'when authenticated as developer' do
@@ -665,10 +702,10 @@ RSpec.describe ProjectsController do
expect(housekeeping).not_to receive(:execute)
post :housekeeping,
- params: {
- namespace_id: project.namespace.path,
- id: project.path
- }
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
expect(response).to have_gitlab_http_status(:found)
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index d0439a18158..b217b100349 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RegistrationsController do
+RSpec.describe RegistrationsController, feature_category: :user_profile do
include TermsHelper
include FullNameHelper
@@ -215,11 +215,18 @@ RSpec.describe RegistrationsController do
property: member.id.to_s,
user: member.reload.user
)
+
+ expect_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'create_user',
+ label: 'invited',
+ user: member.reload.user
+ )
end
end
context 'when member does not exist from the session key value' do
- let(:originating_member_id) { -1 }
+ let(:originating_member_id) { nil }
it 'does not track invite acceptance' do
subject
@@ -229,6 +236,13 @@ RSpec.describe RegistrationsController do
action: 'accepted',
label: 'invite_email'
)
+
+ expect_snowplow_event(
+ category: 'RegistrationsController',
+ action: 'create_user',
+ label: 'signup',
+ user: member.reload.user
+ )
end
end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index c6a8cee2f70..6fa1d93265d 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -37,16 +37,16 @@ RSpec.describe RootController do
user.dashboard = 'stars'
end
- it 'redirects to their specified dashboard' do
+ it 'redirects to their starred projects list' do
get :index
expect(response).to redirect_to starred_dashboard_projects_path
end
end
- context 'who has customized their dashboard setting for project activities' do
+ context 'who has customized their dashboard setting for their own activities' do
before do
- user.dashboard = 'project_activity'
+ user.dashboard = 'your_activity'
end
it 'redirects to the activity list' do
@@ -56,12 +56,24 @@ RSpec.describe RootController do
end
end
+ context 'who has customized their dashboard setting for project activities' do
+ before do
+ user.dashboard = 'project_activity'
+ end
+
+ it 'redirects to the projects activity list' do
+ get :index
+
+ expect(response).to redirect_to activity_dashboard_path(filter: 'projects')
+ end
+ end
+
context 'who has customized their dashboard setting for starred project activities' do
before do
user.dashboard = 'starred_project_activity'
end
- it 'redirects to the activity list' do
+ it 'redirects to their starred projects activity list' do
get :index
expect(response).to redirect_to activity_dashboard_path(filter: 'starred')
@@ -73,7 +85,7 @@ RSpec.describe RootController do
user.dashboard = 'followed_user_activity'
end
- it 'redirects to the activity list' do
+ it 'redirects to the followed users activity list' do
get :index
expect(response).to redirect_to activity_dashboard_path(filter: 'followed')
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 37fc5a033ba..0f7f4a1910b 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchController do
+RSpec.describe SearchController, feature_category: :global_search do
include ExternalAuthorizationServiceHelpers
context 'authorized user' do
@@ -359,12 +359,13 @@ RSpec.describe SearchController do
end.to raise_error(ActionController::ParameterMissing)
end
- it 'sets private cache control headers' do
+ it 'sets correct cache control headers' do
get :count, params: { search: 'hello', scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Cache-Control']).to eq('max-age=60, private')
+ expect(response.headers['Pragma']).to be_nil
end
it 'does NOT blow up if search param is NOT a string' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 78b3cc63b08..1f7d169bae5 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -221,7 +221,7 @@ RSpec.describe SessionsController do
expect(Gitlab::Metrics).to receive(:counter)
.with(:successful_login_captcha_total, anything)
.and_return(counter)
- expect(Gitlab::Metrics).to receive(:counter).and_call_original
+ expect(Gitlab::Metrics).to receive(:counter).at_least(1).time.and_call_original
post(:create, params: { user: user_params }, session: sesion_params)
end
diff --git a/spec/db/development/create_base_work_item_types_spec.rb b/spec/db/development/create_base_work_item_types_spec.rb
index 914b84d8668..7652ccdc487 100644
--- a/spec/db/development/create_base_work_item_types_spec.rb
+++ b/spec/db/development/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create base work item types in development' do
+RSpec.describe 'Create base work item types in development', feature_category: :team_planning do
subject { load Rails.root.join('db', 'fixtures', 'development', '001_create_base_work_item_types.rb') }
it_behaves_like 'work item base types importer'
diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb
index 5960b8bebcc..8d4cb3ac5ef 100644
--- a/spec/db/docs_spec.rb
+++ b/spec/db/docs_spec.rb
@@ -2,13 +2,6 @@
require 'spec_helper'
-# This list is used to provide temporary exceptions for feature categories
-# that are transitioning and not yet in the feature_categories.yml file
-# any additions here should be accompanied by a link to an issue link
-VALID_FEATURE_CATEGORIES = [
- 'jihu' # https://gitlab.com/gitlab-org/database-team/team-tasks/-/issues/192
-].freeze
-
RSpec.shared_examples 'validate dictionary' do |objects, directory_path, required_fields|
context 'for each object' do
let(:directory_path) { directory_path }
@@ -32,6 +25,19 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
end
end
+ # This list is used to provide temporary exceptions for feature categories
+ # that are transitioning and not yet in the feature_categories.yml file
+ # any additions here should be accompanied by a link to an issue link
+ let(:valid_feature_categories) do
+ [
+ 'jihu' # https://gitlab.com/gitlab-org/database-team/team-tasks/-/issues/192
+ ]
+ end
+
+ let(:all_feature_categories) do
+ YAML.load_file(Rails.root.join('config/feature_categories.yml')) + valid_feature_categories
+ end
+
let(:objects_without_metadata) do
objects.reject { |t| metadata.has_key?(t) }
end
@@ -68,9 +74,15 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
end
it 'has a valid feature category' do
+ message = <<~TEXT.chomp
+ Please use a category from https://about.gitlab.com/handbook/product/categories/#categories-a-z
+
+ Table metadata files with an invalid feature category
+ TEXT
+
expect(objects_with_invalid_feature_category).to be_empty, object_metadata_errors(
- 'Table metadata files with an invalid feature category',
- :error,
+ message,
+ :invalid_feature_category,
objects_with_invalid_feature_category
)
end
@@ -102,11 +114,10 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
Rails.root.join(object_metadata_file(object_name))
end
- def feature_categories_valid?(object_feature_categories)
+ def invalid_feature_categories(object_feature_categories)
return false unless object_feature_categories.present?
- all_feature_categories = YAML.load_file(Rails.root.join('config/feature_categories.yml')) + VALID_FEATURE_CATEGORIES
- object_feature_categories.all? { |category| all_feature_categories.include?(category) }
+ object_feature_categories - all_feature_categories
end
def load_object_metadata(required_fields, object_name)
@@ -125,10 +136,8 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
if required_fields.include?(:feature_categories)
object_feature_categories = result.dig(:metadata, :feature_categories)
- unless feature_categories_valid?(object_feature_categories)
- result[:invalid_feature_category] =
- "invalid feature category: #{object_feature_categories}" \
- "Please use a category from https://about.gitlab.com/handbook/product/categories/#categories-a-z"
+ if (invalid = invalid_feature_categories(object_feature_categories)).any?
+ result[:invalid_feature_category] = "invalid feature category: #{invalid.join(', ')}"
end
end
rescue Psych::SyntaxError => ex
diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb
index a5449c6dccd..b7a4a302290 100644
--- a/spec/db/migration_spec.rb
+++ b/spec/db/migration_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Migrations Validation', feature_category: :database do
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],
+ 2022_01_26_21_06_58..2023_01_11_12_45_12 => Gitlab::Database::Migration[2.0],
2021_09_01_15_33_24..2022_04_25_12_06_03 => Gitlab::Database::Migration[1.0],
2021_05_31_05_39_16..2021_09_01_15_33_24 => ActiveRecord::Migration[6.1],
..2021_05_31_05_39_16 => ActiveRecord::Migration[6.0]
diff --git a/spec/db/production/create_base_work_item_types_spec.rb b/spec/db/production/create_base_work_item_types_spec.rb
index 81d80104bb4..f6c3b0f6395 100644
--- a/spec/db/production/create_base_work_item_types_spec.rb
+++ b/spec/db/production/create_base_work_item_types_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create base work item types in production' do
+RSpec.describe 'Create base work item types in production', feature_category: :team_planning do
subject { load Rails.root.join('db', 'fixtures', 'production', '003_create_base_work_item_types.rb') }
it_behaves_like 'work item base types importer'
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 7f3cab55d5a..6019f10eeeb 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -10,7 +10,10 @@ RSpec.describe 'Database schema', feature_category: :database do
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
IGNORED_INDEXES_ON_FKS = {
- slack_integrations_scopes: %w[slack_api_scope_id]
+ slack_integrations_scopes: %w[slack_api_scope_id],
+ # Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/391312
+ approval_project_rules: %w[scan_result_policy_id],
+ approval_merge_request_rules: %w[scan_result_policy_id]
}.with_indifferent_access.freeze
TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
@@ -33,26 +36,26 @@ RSpec.describe 'Database schema', feature_category: :database do
chat_names: %w[chat_id team_id user_id integration_id],
chat_teams: %w[team_id],
ci_build_needs: %w[partition_id],
- ci_build_pending_states: %w[partition_id],
+ ci_build_pending_states: %w[partition_id build_id],
ci_build_report_results: %w[partition_id],
- ci_build_trace_chunks: %w[partition_id],
+ ci_build_trace_chunks: %w[partition_id build_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 runner_machine_id], # NOTE: FK will be added in follow-up https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108167
+ ci_builds_runner_session: %w[partition_id build_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_resources: %w[partition_id build_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],
+ ci_unit_test_failures: %w[partition_id build_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],
@@ -80,6 +83,7 @@ RSpec.describe 'Database schema', feature_category: :database do
ldap_group_links: %w[group_id],
members: %w[source_id created_by_id],
merge_requests: %w[last_edited_by_id state_id],
+ merge_requests_compliance_violations: %w[target_project_id],
merge_request_diff_commits: %w[commit_author_id committer_id],
namespaces: %w[owner_id parent_id],
notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id confirmed_by_id discussion_id],
@@ -89,6 +93,7 @@ RSpec.describe 'Database schema', feature_category: :database do
oauth_applications: %w[owner_id],
product_analytics_events_experimental: %w[event_id txn_id user_id],
project_build_artifacts_size_refreshes: %w[last_job_artifact_id],
+ project_data_transfers: %w[project_id namespace_id],
project_error_tracking_settings: %w[sentry_project_id],
project_group_links: %w[group_id],
project_statistics: %w[namespace_id],
@@ -185,7 +190,6 @@ RSpec.describe 'Database schema', feature_category: :database do
# These pre-existing enums have limits > 2 bytes
IGNORED_LIMIT_ENUMS = {
'Analytics::CycleAnalytics::Stage' => %w[start_event_identifier end_event_identifier],
- 'Analytics::CycleAnalytics::ProjectStage' => %w[start_event_identifier end_event_identifier],
'Ci::Bridge' => %w[failure_reason],
'Ci::Build' => %w[failure_reason],
'Ci::BuildMetadata' => %w[timeout_source],
diff --git a/spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb b/spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb
deleted file mode 100644
index 596791308a4..00000000000
--- a/spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe VideoTutorialsContinuousOnboardingExperiment do
- it "defines a control and candidate" do
- expect(subject.behaviors.keys).to match_array(%w[control candidate])
- end
-end
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 4ae9b4def8e..355fb142994 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -6,5 +6,6 @@ FactoryBot.define do
user
message { 'User sends spam' }
reported_from_url { 'http://gitlab.com' }
+ links_to_spam { ['https://gitlab.com/issue1', 'https://gitlab.com/issue2'] }
end
end
diff --git a/spec/factories/airflow/dags.rb b/spec/factories/airflow/dags.rb
new file mode 100644
index 00000000000..ca4276e2c8f
--- /dev/null
+++ b/spec/factories/airflow/dags.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+FactoryBot.define do
+ factory :airflow_dags, class: '::Airflow::Dags' do
+ sequence(:dag_name) { |n| "dag_name_#{n}" }
+
+ project
+ end
+end
diff --git a/spec/factories/analytics/cycle_analytics/project_stages.rb b/spec/factories/analytics/cycle_analytics/project_stages.rb
deleted file mode 100644
index e673c4957b0..00000000000
--- a/spec/factories/analytics/cycle_analytics/project_stages.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :cycle_analytics_project_stage, class: 'Analytics::CycleAnalytics::ProjectStage' do
- project
- sequence(:name) { |n| "Stage ##{n}" }
- hidden { false }
- issue_stage
- value_stream { association(:cycle_analytics_project_value_stream, project: project) }
-
- trait :issue_stage do
- start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier }
- end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd.identifier }
- end
- end
-end
diff --git a/spec/factories/analytics/cycle_analytics/project_value_streams.rb b/spec/factories/analytics/cycle_analytics/project_value_streams.rb
deleted file mode 100644
index 45a6470b0aa..00000000000
--- a/spec/factories/analytics/cycle_analytics/project_value_streams.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :cycle_analytics_project_value_stream, class: 'Analytics::CycleAnalytics::ProjectValueStream' do
- sequence(:name) { |n| "Value Stream ##{n}" }
-
- project
- end
-end
diff --git a/spec/factories/analytics/cycle_analytics/stages.rb b/spec/factories/analytics/cycle_analytics/stages.rb
new file mode 100644
index 00000000000..4f6f38f6f33
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/stages.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_stage, class: 'Analytics::CycleAnalytics::Stage' do
+ transient do
+ project { nil }
+ end
+
+ sequence(:name) { |n| "Stage ##{n}" }
+ start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier }
+ end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier }
+
+ namespace { association(:group) }
+ value_stream { association(:cycle_analytics_value_stream, namespace: namespace) }
+
+ after(:build) do |stage, evaluator|
+ stage.namespace = evaluator.project.reload.project_namespace if evaluator.project
+ end
+ end
+end
diff --git a/spec/factories/analytics/cycle_analytics/value_streams.rb b/spec/factories/analytics/cycle_analytics/value_streams.rb
new file mode 100644
index 00000000000..fcf8d8339ed
--- /dev/null
+++ b/spec/factories/analytics/cycle_analytics/value_streams.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :cycle_analytics_value_stream, class: 'Analytics::CycleAnalytics::ValueStream' do
+ sequence(:name) { |n| "Value Stream ##{n}" }
+
+ namespace { association(:group) }
+ end
+end
diff --git a/spec/factories/bulk_import/entities.rb b/spec/factories/bulk_import/entities.rb
index 66d212daaae..a662d42c7c9 100644
--- a/spec/factories/bulk_import/entities.rb
+++ b/spec/factories/bulk_import/entities.rb
@@ -8,7 +8,7 @@ FactoryBot.define do
sequence(:source_full_path) { |n| "source-path-#{n}" }
sequence(:destination_namespace) { |n| "destination-path-#{n}" }
- destination_name { 'Imported Entity' }
+ destination_slug { 'imported-entity' }
sequence(:source_xid)
migrate_projects { true }
diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb
index 6cbcabca7ab..49ac74f6f86 100644
--- a/spec/factories/ci/bridge.rb
+++ b/spec/factories/ci/bridge.rb
@@ -33,6 +33,14 @@ FactoryBot.define do
end
end
+ trait :retried do
+ retried { true }
+ end
+
+ trait :retryable do
+ success
+ end
+
trait :created do
status { 'created' }
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 78398fd7f20..224f460488b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -7,7 +7,6 @@ FactoryBot.define do
created_at { 'Di 29. Okt 09:50:00 CET 2013' }
scheduling_type { 'stage' }
pending
- partition_id { pipeline.partition_id }
options do
{
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index eef5c593e0f..d68562c0aa5 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -237,7 +237,9 @@ FactoryBot.define do
trait :with_job do
after(:build) do |pipeline, evaluator|
- pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
+ stage = build(:ci_stage, pipeline: pipeline)
+
+ pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project, ci_stage: stage)
end
end
diff --git a/spec/factories/ci/processable.rb b/spec/factories/ci/processable.rb
index 76c7376d24a..49e66368f94 100644
--- a/spec/factories/ci/processable.rb
+++ b/spec/factories/ci/processable.rb
@@ -3,13 +3,37 @@
FactoryBot.define do
factory :ci_processable, class: 'Ci::Processable' do
name { 'processable' }
- stage { 'test' }
stage_idx { ci_stage.try(:position) || 0 }
ref { 'master' }
tag { false }
pipeline factory: :ci_pipeline
project { pipeline.project }
scheduling_type { 'stage' }
+ partition_id { pipeline.partition_id }
+
+ # This factory was updated to help with the efforts of the removal of `ci_builds.stage`:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/364377
+ # These additions can be removed once the specs that use the stage attribute have been updated
+
+ transient do
+ stage { 'test' }
+ end
+
+ after(:build) do |processable, evaluator|
+ processable.stage = evaluator.stage
+ end
+
+ before(:create) do |processable, evaluator|
+ next if processable.ci_stage
+
+ if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
+ processable.ci_stage = ci_stage
+ else
+ processable.ci_stage = create(:ci_stage, pipeline: processable.pipeline,
+ project: processable.project || evaluator.project,
+ name: evaluator.stage, position: evaluator.stage_idx, status: 'created')
+ end
+ end
trait :waiting_for_resource do
status { 'waiting_for_resource' }
diff --git a/spec/factories/ci/runner_machines.rb b/spec/factories/ci/runner_machines.rb
index 09bf5d0844e..9d601caa634 100644
--- a/spec/factories/ci/runner_machines.rb
+++ b/spec/factories/ci/runner_machines.rb
@@ -3,6 +3,11 @@
FactoryBot.define do
factory :ci_runner_machine, class: 'Ci::RunnerMachine' do
runner factory: :ci_runner
- machine_xid { "r_#{SecureRandom.hex.slice(0, 10)}" }
+ system_xid { "r_#{SecureRandom.hex.slice(0, 10)}" }
+
+ trait :stale do
+ created_at { 1.year.ago }
+ contacted_at { Ci::RunnerMachine::STALE_TIMEOUT.ago }
+ end
end
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 6a21df943f5..0647058d63a 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -98,11 +98,6 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
- factory :clusters_applications_cert_manager, class: 'Clusters::Applications::CertManager' do
- email { 'admin@example.com' }
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
-
factory :clusters_applications_crossplane, class: 'Clusters::Applications::Crossplane' do
stack { 'gcp' }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
@@ -125,9 +120,5 @@ FactoryBot.define do
oauth_application factory: :oauth_application
cluster factory: %i(cluster with_installed_helm provided_by_gcp project)
end
-
- factory :clusters_applications_cilium, class: 'Clusters::Applications::Cilium' do
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
end
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 72424a3c321..32cd6beb7ea 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -94,13 +94,11 @@ FactoryBot.define do
trait :with_all_applications do
application_helm factory: %i(clusters_applications_helm installed)
application_ingress factory: %i(clusters_applications_ingress installed)
- application_cert_manager factory: %i(clusters_applications_cert_manager installed)
application_crossplane factory: %i(clusters_applications_crossplane installed)
application_prometheus factory: %i(clusters_applications_prometheus installed)
application_runner factory: %i(clusters_applications_runner installed)
application_jupyter factory: %i(clusters_applications_jupyter installed)
application_knative factory: %i(clusters_applications_knative installed)
- application_cilium factory: %i(clusters_applications_cilium installed)
end
trait :with_domain do
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index a60f0a3879a..7d0176d0683 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -10,6 +10,7 @@ FactoryBot.define do
pipeline factory: :ci_pipeline
started_at { 'Tue, 26 Jan 2016 08:21:42 +0100' }
finished_at { 'Tue, 26 Jan 2016 08:23:42 +0100' }
+ partition_id { pipeline&.partition_id }
trait :success do
status { 'success' }
diff --git a/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb b/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb
new file mode 100644
index 00000000000..a61b5cde7a0
--- /dev/null
+++ b/spec/factories/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :postgres_async_foreign_key_validation,
+ class: 'Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation' do
+ sequence(:name) { |n| "fk_users_id_#{n}" }
+ table_name { "users" }
+ end
+end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index 59c6ea5f55a..630f775d231 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -20,4 +20,11 @@ FactoryBot.define do
trait :object_storage do
file_store { LfsObjectUploader::Store::REMOTE }
end
+
+ trait :with_lfs_object_dot_iso_file do
+ with_file
+ object_storage
+ oid { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ size { 133 }
+ end
end
diff --git a/spec/factories/member_roles.rb b/spec/factories/member_roles.rb
index 08df45a85f8..503438d2521 100644
--- a/spec/factories/member_roles.rb
+++ b/spec/factories/member_roles.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
namespace { association(:group) }
base_access_level { Gitlab::Access::DEVELOPER }
- trait(:guest) { base_access_level { GroupMember::GUEST } }
+ trait(:developer) { base_access_level { Gitlab::Access::DEVELOPER } }
+ trait(:guest) { base_access_level { Gitlab::Access::GUEST } }
end
end
diff --git a/spec/factories/packages/debian/distribution.rb b/spec/factories/packages/debian/distribution.rb
index 2142dba974b..48892d16efb 100644
--- a/spec/factories/packages/debian/distribution.rb
+++ b/spec/factories/packages/debian/distribution.rb
@@ -4,22 +4,24 @@ FactoryBot.define do
factory :debian_project_distribution, class: 'Packages::Debian::ProjectDistribution' do
container { association(:project) }
- sequence(:codename) { |n| "project-dist-#{n}" }
+ sequence(:codename) { |n| "#{FFaker::Lorem.word}#{n}" }
factory :debian_group_distribution, class: 'Packages::Debian::GroupDistribution' do
container { association(:group) }
+ end
- sequence(:codename) { |n| "group-dist-#{n}" }
+ trait(:with_suite) do
+ sequence(:suite) { |n| "#{FFaker::Lorem.word}#{n}" }
end
trait(:with_file) do
file_signature do
- <<~EOF
+ <<~FILESIGNATURE
-----BEGIN PGP SIGNATURE-----
ABC
-----BEGIN PGP SIGNATURE-----
- EOF
+ FILESIGNATURE
end
after(:build) do |distribution, evaluator|
diff --git a/spec/factories/packages/debian/group_architecture.rb b/spec/factories/packages/debian/group_architecture.rb
index 2582faae4ed..6f5267c7860 100644
--- a/spec/factories/packages/debian/group_architecture.rb
+++ b/spec/factories/packages/debian/group_architecture.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :debian_group_architecture, class: 'Packages::Debian::GroupArchitecture' do
distribution { association(:debian_group_distribution) }
- sequence(:name) { |n| "group-arch-#{n}" }
+ sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
end
end
diff --git a/spec/factories/packages/debian/group_component.rb b/spec/factories/packages/debian/group_component.rb
index 92d438be389..f3605991a32 100644
--- a/spec/factories/packages/debian/group_component.rb
+++ b/spec/factories/packages/debian/group_component.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :debian_group_component, class: 'Packages::Debian::GroupComponent' do
distribution { association(:debian_group_distribution) }
- sequence(:name) { |n| "group-component-#{n}" }
+ sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
end
end
diff --git a/spec/factories/packages/debian/project_architecture.rb b/spec/factories/packages/debian/project_architecture.rb
index d6985da4128..2bba8de2e7d 100644
--- a/spec/factories/packages/debian/project_architecture.rb
+++ b/spec/factories/packages/debian/project_architecture.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :debian_project_architecture, class: 'Packages::Debian::ProjectArchitecture' do
distribution { association(:debian_project_distribution) }
- sequence(:name) { |n| "project-arch-#{n}" }
+ sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
end
end
diff --git a/spec/factories/packages/debian/project_component.rb b/spec/factories/packages/debian/project_component.rb
index a56aec4cef0..cb7501cd9f2 100644
--- a/spec/factories/packages/debian/project_component.rb
+++ b/spec/factories/packages/debian/project_component.rb
@@ -4,6 +4,6 @@ FactoryBot.define do
factory :debian_project_component, class: 'Packages::Debian::ProjectComponent' do
distribution { association(:debian_project_distribution) }
- sequence(:name) { |n| "project-component-#{n}" }
+ sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
end
end
diff --git a/spec/factories/packages/packages.rb b/spec/factories/packages/packages.rb
index 1da4f0cedbc..d0fde0a16cd 100644
--- a/spec/factories/packages/packages.rb
+++ b/spec/factories/packages/packages.rb
@@ -66,13 +66,13 @@ FactoryBot.define do
end
factory :debian_package do
- sequence(:name) { |n| "package-#{n}" }
+ sequence(:name) { |n| "#{FFaker::Lorem.word}#{n}" }
sequence(:version) { |n| "1.0-#{n}" }
package_type { :debian }
transient do
without_package_files { false }
- file_metadatum_trait { :keep }
+ file_metadatum_trait { processing? ? :unknown : :keep }
published_in { :create }
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 6e3a7a3f5ef..f113ca2425f 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -253,9 +253,35 @@ FactoryBot.define do
create_templates { nil }
create_branch { nil }
create_tag { nil }
+ lfs { false }
end
after :create do |project, evaluator|
+ # Specify `lfs: true` to create the LfsObject for the LFS file in the test repo:
+ # https://gitlab.com/gitlab-org/gitlab-test/-/blob/master/files/lfs/lfs_object.iso
+ if evaluator.lfs
+ RSpec::Mocks.with_temporary_scope do
+ # If lfs object store is disabled we need to mock
+ unless Gitlab.config.lfs.object_store.enabled
+ config = Gitlab.config.lfs.object_store.merge('enabled' => true)
+ allow(LfsObjectUploader).to receive(:object_store_options).and_return(config)
+ Fog.mock!
+ Fog::Storage.new(LfsObjectUploader.object_store_credentials).tap do |connection|
+ connection.directories.create(key: config.remote_directory) # rubocop:disable Rails/SaveBang
+
+ # Cleanup remaining files
+ connection.directories.each do |directory|
+ directory.files.map(&:destroy)
+ end
+ rescue Excon::Error::Conflict
+ end
+ end
+
+ lfs_object = create(:lfs_object, :with_lfs_object_dot_iso_file)
+ create(:lfs_objects_project, project: project, lfs_object: lfs_object)
+ end
+ end
+
if evaluator.create_templates
templates_path = "#{evaluator.create_templates}_templates"
@@ -286,7 +312,6 @@ FactoryBot.define do
"README on branch #{evaluator.create_branch}",
message: 'Add README.md',
branch_name: evaluator.create_branch)
-
end
if evaluator.create_tag
@@ -441,7 +466,9 @@ FactoryBot.define do
trait :with_jira_integration do
has_external_issue_tracker { true }
- jira_integration
+ after :create do |project|
+ create(:jira_integration, project: project)
+ end
end
trait :with_prometheus_integration do
diff --git a/spec/factories/projects/data_transfers.rb b/spec/factories/projects/data_transfers.rb
new file mode 100644
index 00000000000..4184f475663
--- /dev/null
+++ b/spec/factories/projects/data_transfers.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_data_transfer, class: 'Projects::DataTransfer' do
+ project factory: :project
+ namespace { project.root_namespace }
+ date { Time.current.utc.beginning_of_month }
+ end
+end
diff --git a/spec/factories/protected_tags/create_access_levels.rb b/spec/factories/protected_tags/create_access_levels.rb
new file mode 100644
index 00000000000..07450b2789c
--- /dev/null
+++ b/spec/factories/protected_tags/create_access_levels.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :protected_tag_create_access_level, class: 'ProtectedTag::CreateAccessLevel' do
+ deploy_key { nil }
+ protected_tag
+ access_level { Gitlab::Access::DEVELOPER }
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 2b53a469841..e641f925758 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -180,6 +180,10 @@ FactoryBot.define do
provider { 'ldapmain' }
end
end
+
+ trait :unconfirmed do
+ confirmed_at { nil }
+ end
end
factory :atlassian_user do
diff --git a/spec/factories/work_items/widget_definitions.rb b/spec/factories/work_items/widget_definitions.rb
new file mode 100644
index 00000000000..bbd7c1e7432
--- /dev/null
+++ b/spec/factories/work_items/widget_definitions.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :widget_definition, class: 'WorkItems::WidgetDefinition' do
+ work_item_type
+ namespace
+
+ name { 'Description' }
+ widget_type { 'description' }
+ end
+end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 393cd6f6a21..e53f0cd936f 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -18,6 +18,8 @@ require 'active_support/dependencies'
require_relative '../config/initializers/0_inject_enterprise_edition_module'
require_relative '../config/settings'
require_relative 'support/rspec'
+require_relative '../lib/gitlab/utils'
+require_relative '../lib/gitlab/utils/strong_memoize'
require 'active_support/all'
require_relative 'simplecov_env'
diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb
index e0a61656a88..1267025a7bf 100644
--- a/spec/features/abuse_report_spec.rb
+++ b/spec/features/abuse_report_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
let_it_be(:incident) { create(:incident, project: project, author: abusive_user) }
before do
- visit project_issues_incident_path(project, incident)
+ visit incident_project_issues_path(project, incident)
click_button 'Incident actions'
end
@@ -82,7 +82,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
visit user_path(abusive_user)
- fill_and_submit_abuse_category_form("They're being offsensive or abusive.")
+ fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
@@ -136,7 +136,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
click_button 'More actions'
end
- it_behaves_like 'reports the user without an abuse category'
+ it_behaves_like 'reports the user with an abuse category'
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 5fbe7039c1d..252d9ac5bac 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -19,6 +19,9 @@ RSpec.describe 'Admin Appearance', feature_category: :not_owned do
fill_in 'appearance_title', with: 'MyCompany'
fill_in 'appearance_description', with: 'dev server'
+ fill_in 'appearance_pwa_name', with: 'GitLab PWA'
+ fill_in 'appearance_pwa_short_name', with: 'GitLab'
+ fill_in 'appearance_pwa_description', with: 'GitLab as PWA'
fill_in 'appearance_new_project_guidelines', with: 'Custom project guidelines'
fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines'
click_button 'Update appearance settings'
@@ -28,6 +31,9 @@ RSpec.describe 'Admin Appearance', feature_category: :not_owned do
expect(page).to have_field('appearance_title', with: 'MyCompany')
expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_field('appearance_pwa_name', with: 'GitLab PWA')
+ expect(page).to have_field('appearance_pwa_short_name', with: 'GitLab')
+ expect(page).to have_field('appearance_pwa_description', with: 'GitLab as PWA')
expect(page).to have_field('appearance_new_project_guidelines', with: 'Custom project guidelines')
expect(page).to have_field('appearance_profile_image_guidelines', with: 'Custom profile image guidelines')
expect(page).to have_content 'Last edit'
@@ -135,6 +141,19 @@ RSpec.describe 'Admin Appearance', feature_category: :not_owned do
expect(page).not_to have_css(logo_selector)
end
+ it 'appearance pwa icon' do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ visit admin_application_settings_appearances_path
+
+ attach_file(:appearance_pwa_icon, logo_fixture)
+ click_button 'Update appearance settings'
+ expect(page).to have_css(pwa_icon_selector)
+
+ click_link 'Remove icon'
+ expect(page).not_to have_css(pwa_icon_selector)
+ end
+
it 'header logos' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@@ -183,6 +202,10 @@ RSpec.describe 'Admin Appearance', feature_category: :not_owned do
'//img[data-src^="/uploads/-/system/appearance/logo"]'
end
+ def pwa_icon_selector
+ '//img[data-src^="/uploads/-/system/appearance/pwa_icon"]'
+ end
+
def header_logo_selector
'//img[data-src^="/uploads/-/system/appearance/header_logo"]'
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 119e09f9b09..a07a5c48713 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe 'Admin Groups', feature_category: :subgroups do
- include Select2Helper
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index e6630e40147..363c152371e 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -105,8 +105,8 @@ RSpec.describe 'Admin::Hooks', feature_category: :integrations do
WebMock.stub_request(:post, system_hook.url)
visit admin_hooks_path
- find('.hook-test-button.dropdown').click
- click_link 'Push events'
+ click_button 'Test'
+ click_button 'Push events'
end
it { expect(page).to have_current_path(admin_hooks_path, ignore_query: true) }
@@ -141,8 +141,8 @@ RSpec.describe 'Admin::Hooks', feature_category: :integrations do
create(:merge_request, source_project: project)
visit admin_hooks_path
- find('.hook-test-button.dropdown').click
- click_link 'Merge request events'
+ click_button 'Test'
+ click_button '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 f0eaa83f05e..d46b314c144 100644
--- a/spec/features/admin/admin_jobs_spec.rb
+++ b/spec/features/admin/admin_jobs_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Admin Jobs', feature_category: :continuous_integration do
before do
+ stub_feature_flags(admin_jobs_vue: false)
admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@@ -28,8 +29,8 @@ RSpec.describe 'Admin Jobs', feature_category: :continuous_integration do
expect(page).to have_button 'Stop all jobs'
click_button 'Stop all jobs'
- expect(page).to have_button 'Stop jobs'
- expect(page).to have_content 'Stop all jobs?'
+ expect(page).to have_button 'Yes, proceed'
+ expect(page).to have_content 'Are you sure?'
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 3c7eba2cc97..f08e6521184 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -6,6 +6,7 @@ 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
+ include ListboxHelpers
let_it_be_with_reload(:user) { create :user }
let_it_be_with_reload(:project) { create(:project, :with_namespace_settings) }
@@ -117,8 +118,7 @@ RSpec.describe "Admin::Projects", feature_category: :projects do
it 'transfers project to group web', :js do
visit admin_project_path(project)
- click_button 'Search for Namespace'
- click_button 'group: web'
+ select_from_listbox 'group: web', from: 'Search for Namespace'
click_button 'Transfer'
expect(page).to have_content("Web / #{project.name}")
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 30fd04b1c3e..04dc206f052 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -21,13 +21,27 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace, creator: user) }
+ describe "runners creation" do
+ before do
+ stub_feature_flags(create_runner_workflow: true)
+
+ visit admin_runners_path
+ end
+
+ it 'shows a create button' do
+ expect(page).to have_link s_('Runner|New instance runner'), href: new_admin_runner_path
+ end
+ end
+
describe "runners registration" do
before do
+ stub_feature_flags(create_runner_workflow: false)
+
visit admin_runners_path
end
it_behaves_like "shows and resets runner registration token" do
- let(:dropdown_text) { 'Register an instance runner' }
+ let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
end
@@ -65,7 +79,6 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
it 'has all necessary texts' do
- expect(page).to have_text "Register an instance runner"
expect(page).to have_text "#{s_('Runners|All')} 3"
expect(page).to have_text "#{s_('Runners|Online')} 1"
expect(page).to have_text "#{s_('Runners|Offline')} 2"
@@ -491,6 +504,8 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
)
end
+ let_it_be(:runner_job) { create(:ci_build, runner: runner) }
+
before do
visit admin_runner_path(runner)
end
@@ -517,6 +532,11 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ it_behaves_like 'shows runner jobs tab' do
+ let(:job_count) { '1' }
+ let(:job) { runner_job }
+ end
+
describe 'when a runner is deleted' do
before do
click_on 'Delete runner'
@@ -644,7 +664,7 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
visit edit_admin_runner_path(runner)
end
- it 'removed specific runner from project' do
+ it 'removed project runner from project' do
within '[data-testid="assigned-projects"]' do
click_on 'Disable'
end
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
index 4b8636da6b4..cad1bf74d2e 100644
--- a/spec/features/admin/admin_sees_background_migrations_spec.rb
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -92,11 +92,11 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
expect(page).not_to have_content('Paused')
expect(page).to have_content('Active')
- click_button('Pause')
+ click_on('Pause')
expect(page).not_to have_content('Active')
expect(page).to have_content('Paused')
- click_button('Resume')
+ click_on('Resume')
expect(page).not_to have_content('Paused')
expect(page).to have_content('Active')
end
@@ -123,7 +123,7 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
tab = find_link 'Failed'
tab.click
- expect(page).to have_selector("[method='post'][action='/admin/background_migrations/#{failed_migration.id}/retry?database=main']")
+ expect(page).to have_selector("[data-method='post'][href='/admin/background_migrations/#{failed_migration.id}/retry?database=main']")
end
end
@@ -144,7 +144,7 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
expect(page).to have_content('0.00%')
expect(page).to have_content(failed_migration.status_name.to_s)
- click_button('Retry')
+ click_on('Retry')
expect(page).not_to have_content(failed_migration.job_class_name)
expect(page).not_to have_content(failed_migration.table_name)
expect(page).not_to have_content('0.00%')
@@ -172,7 +172,7 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
end
it 'can change tabs and retain database param' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
visit admin_background_migrations_path(database: 'ci')
@@ -212,7 +212,7 @@ RSpec.describe "Admin > Admin sees background migrations", feature_category: :da
context 'when multi database is enabled' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
allow(Gitlab::Database).to receive(:db_config_names).and_return(%w[main ci])
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 2ac86ab9f49..26efed85513 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
end
it 'change visibility settings' do
- page.within('.as-visibility-access') do
+ page.within('[data-testid="admin-visibility-access-settings"]') do
choose "application_setting_default_project_visibility_20"
click_button 'Save changes'
end
@@ -33,23 +33,29 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
end
it 'uncheck all restricted visibility levels' do
- page.within('.as-visibility-access') do
- find('#application_setting_restricted_visibility_levels_0').set(false)
- find('#application_setting_restricted_visibility_levels_10').set(false)
- find('#application_setting_restricted_visibility_levels_20').set(false)
+ page.within('[data-testid="restricted-visibility-levels"]') do
+ uncheck s_('VisibilityLevel|Public')
+ uncheck s_('VisibilityLevel|Internal')
+ uncheck s_('VisibilityLevel|Private')
+ end
+
+ page.within('[data-testid="admin-visibility-access-settings"]') do
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
- expect(find('#application_setting_restricted_visibility_levels_0')).not_to be_checked
- expect(find('#application_setting_restricted_visibility_levels_10')).not_to be_checked
- expect(find('#application_setting_restricted_visibility_levels_20')).not_to be_checked
+
+ page.within('[data-testid="restricted-visibility-levels"]') do
+ expect(find_field(s_('VisibilityLevel|Public'))).not_to be_checked
+ expect(find_field(s_('VisibilityLevel|Internal'))).not_to be_checked
+ expect(find_field(s_('VisibilityLevel|Private'))).not_to be_checked
+ end
end
it 'modify import sources' do
expect(current_settings.import_sources).not_to be_empty
- page.within('.as-visibility-access') do
+ page.within('[data-testid="admin-visibility-access-settings"]') do
Gitlab::ImportSources.options.map do |name, _|
uncheck name
end
@@ -60,7 +66,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
expect(page).to have_content "Application settings saved successfully"
expect(current_settings.import_sources).to be_empty
- page.within('.as-visibility-access') do
+ page.within('[data-testid="admin-visibility-access-settings"]') do
check "Repository by URL"
click_button 'Save changes'
end
@@ -70,7 +76,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
end
it 'change Visibility and Access Controls' do
- page.within('.as-visibility-access') do
+ page.within('[data-testid="admin-visibility-access-settings"]') do
page.within('[data-testid="project-export"]') do
uncheck 'Enabled'
end
@@ -88,7 +94,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
end
it 'change Keys settings' do
- page.within('.as-visibility-access') do
+ page.within('[data-testid="admin-visibility-access-settings"]') do
select 'Are forbidden', from: 'RSA SSH keys'
select 'Are allowed', from: 'DSA SSH keys'
select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
@@ -155,19 +161,28 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
context 'when Gitlab.com' do
let(:dot_com?) { true }
- it 'does not expose the setting' do
- expect(page).to have_no_selector('#application_setting_deactivate_dormant_users')
- end
-
- it 'does not expose the setting' do
- expect(page).to have_no_selector('#application_setting_deactivate_dormant_users_period')
+ it 'does not expose the setting section' do
+ # NOTE: not_to have_content may have false positives for content
+ # that might not load instantly, so before checking that
+ # `Dormant users` subsection has _not_ loaded, we check that the
+ # `Account and limit` section _was_ loaded
+ expect(page).to have_content('Account and limit')
+ expect(page).not_to have_content('Dormant users')
+ expect(page).not_to have_field('Deactivate dormant users after a period of inactivity')
+ expect(page).not_to have_field('Days of inactivity before deactivation')
end
end
context 'when not Gitlab.com' do
let(:dot_com?) { false }
- it 'changes Dormant users' do
+ it 'exposes the setting section' do
+ expect(page).to have_content('Dormant users')
+ expect(page).to have_field('Deactivate dormant users after a period of inactivity')
+ expect(page).to have_field('Days of inactivity before deactivation')
+ end
+
+ it 'changes dormant users' do
expect(page).to have_unchecked_field('Deactivate dormant users after a period of inactivity')
expect(current_settings.deactivate_dormant_users).to be_falsey
@@ -184,7 +199,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
expect(page).to have_checked_field('Deactivate dormant users after a period of inactivity')
end
- it 'change Dormant users period' do
+ it 'change dormant users period' do
expect(page).to have_field _('Days of inactivity before deactivation')
page.within(find('[data-testid="account-limit"]')) do
@@ -198,6 +213,27 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
expect(page).to have_field _('Days of inactivity before deactivation'), with: '90'
end
+
+ it 'displays dormant users period field validation error', :js do
+ selector = '#application_setting_deactivate_dormant_users_period_error'
+ expect(page).not_to have_selector(selector, visible: :visible)
+
+ page.within(find('[data-testid="account-limit"]')) do
+ check 'application_setting_deactivate_dormant_users'
+ fill_in _('application_setting_deactivate_dormant_users_period'), with: '30'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_selector(selector, visible: :visible)
+ end
+
+ it 'auto disables dormant users period field depending on parent checkbox', :js do
+ uncheck 'application_setting_deactivate_dormant_users'
+ expect(page).to have_field('application_setting_deactivate_dormant_users_period', disabled: true)
+
+ check 'application_setting_deactivate_dormant_users'
+ expect(page).to have_field('application_setting_deactivate_dormant_users_period', disabled: false)
+ end
end
end
@@ -329,11 +365,13 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
page.within('#js-jira_connect-settings') do
fill_in 'Jira Connect Application ID', with: '1234'
fill_in 'Jira Connect Proxy URL', with: 'https://example.com'
+ check 'Enable public key storage'
click_button 'Save changes'
end
expect(current_settings.jira_connect_application_key).to eq('1234')
expect(current_settings.jira_connect_proxy_url).to eq('https://example.com')
+ expect(current_settings.jira_connect_public_key_storage_enabled).to eq(true)
expect(page).to have_content "Application settings saved successfully"
end
end
@@ -791,9 +829,36 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
context 'Preferences page' do
before do
+ stub_feature_flags(deactivation_email_additional_text: deactivation_email_additional_text_feature_flag)
visit preferences_admin_application_settings_path
end
+ let(:deactivation_email_additional_text_feature_flag) { true }
+
+ describe 'Email page' do
+ context 'when deactivation email additional text feature flag is enabled' do
+ it 'shows deactivation email additional text field' do
+ expect(page).to have_field 'Additional text for deactivation email'
+
+ page.within('.as-email') do
+ fill_in 'Additional text for deactivation email', with: 'So long and thanks for all the fish!'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(current_settings.deactivation_email_additional_text).to eq('So long and thanks for all the fish!')
+ end
+ end
+
+ context 'when deactivation email additional text feature flag is disabled' do
+ let(:deactivation_email_additional_text_feature_flag) { false }
+
+ it 'does not show deactivation email additional text field' do
+ expect(page).not_to have_field 'Additional text for deactivation email'
+ end
+ end
+ end
+
it 'change Help page' do
new_support_url = 'http://example.com/help'
new_documentation_url = 'https://docs.gitlab.com'
@@ -861,7 +926,7 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
context 'Nav bar' do
it 'shows default help links in nav' do
- default_support_url = "https://#{ApplicationHelper.promo_host}/getting-help/"
+ default_support_url = "https://#{ApplicationHelper.promo_host}/get-help/"
visit root_dashboard_path
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index 975af84969d..07db0750074 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Admin::Users', feature_category: :user_management do
include Spec::Support::Helpers::Features::AdminUsersHelpers
include Spec::Support::Helpers::ModalHelpers
+ include ListboxHelpers
let_it_be(:user, reload: true) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:current_user) { create(:admin) }
@@ -608,8 +609,8 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
def sort_by(option)
page.within('.filtered-search-block') do
- find('.gl-dropdown').click
- find('.gl-dropdown-item', text: option).click
+ find('.gl-new-dropdown').click
+ select_listbox_item(option)
end
end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index f1ee7a8fde7..8aecaab42c2 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -130,6 +130,41 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do
end
end
+ context 'ordering in list using move to position' do
+ let(:move_to_position) { find('[data-testid="board-move-to-position"]') }
+
+ before do
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ it 'moves to end of list' do
+ expect(all('.board-card').first).to have_content(issue3.title)
+
+ page.within(find('.board:nth-child(2)')) do
+ first('.board-card').hover
+ move_to_position.click
+
+ click_button 'Move to end of list'
+ end
+
+ expect(all('.board-card').last).to have_content(issue3.title)
+ end
+
+ it 'moves to start of list' do
+ expect(all('.board-card').last).to have_content(issue1.title)
+
+ page.within(find('.board:nth-child(2)')) do
+ all('.board-card').last.hover
+ move_to_position.click
+
+ click_button 'Move to start of list'
+ end
+
+ expect(all('.board-card').first).to have_content(issue1.title)
+ end
+ end
+
context 'ordering when changing list' do
let(:label2) { create(:label, project: project) }
let!(:list2) { create(:list, board: board, label: label2, position: 1) }
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 1b0695e4e60..d597c57ac1c 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -14,6 +14,10 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :team_planning d
let(:board_list_header) { first('[data-testid="board-list-header"]') }
let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') }
+ before do
+ stub_feature_flags(apollo_boards: false)
+ end
+
context 'authorized user' do
before do
project.add_maintainer(user)
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 0ca680c5ed5..036daee7655 100644
--- a/spec/features/boards/reload_boards_on_browser_back_spec.rb
+++ b/spec/features/boards/reload_boards_on_browser_back_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js, feat
context 'authorized user' do
before do
+ stub_feature_flags(apollo_boards: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index c3bb58df797..39485fe21a9 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
let_it_be(:board_list) { create(:backlog_list, board: group_board) }
before do
+ stub_feature_flags(apollo_boards: false)
+
load_board group_board_path(group, group_board)
end
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index 8300cfce539..3e4289347e3 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -23,7 +23,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
end
shared_examples 'a dismissable Broadcast Messages' do
- it 'hides broadcast message after dismiss', :js do
+ it 'hides broadcast message after dismiss', :js,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390900' do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
@@ -31,7 +32,8 @@ RSpec.describe 'Broadcast Messages', feature_category: :onboarding do
expect(page).not_to have_content 'SampleMessage'
end
- it 'broadcast message is still hidden after refresh', :js do
+ it 'broadcast message is still hidden after refresh', :js,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391406' do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 2c5b7d66e2f..b2a29c88b68 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, feature_category: :users do
+RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
include MobileHelpers
let(:user) { create(:user) }
@@ -71,6 +71,7 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :users do
end
before do
+ stub_feature_flags(profile_tabs_vue: false)
sign_in user
end
@@ -145,9 +146,9 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :users 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
+ Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
WorkItems::CreateService.new(
- project: contributed_project,
+ container: contributed_project,
current_user: user,
params: { title: 'new task' },
spam_params: nil
@@ -189,7 +190,7 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :users do
push_code_contribution
travel_to(Date.yesterday) do
- Issues::CreateService.new(project: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
+ Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
end
end
include_context 'visit user page'
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
index ac7b68876da..15c900592a1 100644
--- a/spec/features/callouts/registration_enabled_spec.rb
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe 'Registration enabled callout', feature_category: :authentication
visit root_dashboard_path
end
- it 'does not display callout' do
+ it 'does not display callout', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391192' do
expect(page).not_to have_content callout_title
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index e4d4375a138..eafe74f4b0b 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -24,7 +24,8 @@ RSpec.describe 'Commits', feature_category: :source_code_management do
end
context 'commit status is Generic Commit Status' do
- let!(:status) { create(:generic_commit_status, pipeline: pipeline, ref: pipeline.ref) }
+ let(:stage) { create(:ci_stage, pipeline: pipeline, name: 'external') }
+ let!(:status) { create(:generic_commit_status, pipeline: pipeline, ref: pipeline.ref, ci_stage: stage) }
before do
project.add_reporter(user)
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index edb3dacc2cc..2f9b7bb7e0f 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', feature_category: :users do
+RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do
let(:user) { create(:user) }
before do
@@ -12,9 +12,15 @@ RSpec.describe 'Dashboard > Activity', feature_category: :users do
it_behaves_like 'a dashboard page with sidebar', :activity_dashboard_path, :activity
context 'tabs' do
- it 'shows Your Projects' do
+ it 'shows Your Activity' do
visit activity_dashboard_path
+ expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Your activity')
+ end
+
+ it 'shows Your Projects' do
+ visit activity_dashboard_path(filter: 'projects')
+
expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Your projects')
end
@@ -24,7 +30,7 @@ RSpec.describe 'Dashboard > Activity', feature_category: :users do
expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Starred projects')
end
- it 'shows Followed Projects' do
+ it 'shows Followed Users' do
visit activity_dashboard_path(filter: 'followed')
expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Followed users')
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 34f99765c29..c6e78c8b57c 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, feature_category: :users do
+RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'test', namespace: user.namespace) }
@@ -12,6 +12,10 @@ RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :users do
project.add_maintainer(user)
end
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
context 'on the activity tab' do
before do
Event.create!(project: project, author_id: user.id, action: :joined,
@@ -40,7 +44,7 @@ RSpec.describe 'Tooltips on .timeago dates', :js, feature_category: :users do
wait_for_requests
end
- it 'has the datetime formated correctly' do
+ it 'has the datetime formated correctly', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/334481' do
expect(page).to have_selector('[data-testid=snippet-created-at] .js-timeago', text: '1 day ago')
page.find('[data-testid=snippet-created-at] .js-timeago').hover
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index d5f362d8449..a7734ed50c2 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe 'Dashboard Issues filtering', :js, feature_category: :team_planni
end
end
- def visit_issues(*args)
- visit issues_dashboard_path(*args)
+ def visit_issues(...)
+ visit issues_dashboard_path(...)
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index ae375bd3e13..654cc9978a7 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -51,29 +51,26 @@ RSpec.describe 'Dashboard Issues', feature_category: :team_planning do
describe 'new issue dropdown' do
it 'shows projects only with issues feature enabled', :js do
- click_button 'Toggle project select'
+ click_button _('Select project to create issue')
- page.within('.select2-results') do
+ page.within('[data-testid="new-resource-dropdown"] [role="menu"]') do
expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_issues_disabled.full_name)
end
end
it 'shows the new issue page', :js do
- click_button 'Toggle project select'
+ click_button _('Select project to create issue')
wait_for_requests
project_path = "/#{project.full_path}"
- project_json = { name: project.full_name, url: project_path }.to_json
- # simulate selection, and prevent overlap by dropdown menu
- first('.project-item-select', visible: false)
- execute_script("$('.project-item-select').val('#{project_json}').trigger('change');")
- find('#select2-drop-mask', visible: false)
- execute_script("$('#select2-drop-mask').remove();")
+ page.within('[data-testid="new-resource-dropdown"]') do
+ find_button(project.full_name).click
+ end
- find('.js-new-project-item-link').click
+ click_link format(_('New issue in %{project}'), project: project.name)
expect(page).to have_current_path("#{project_path}/-/issues/new")
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index a146a6987bc..34bab9dffd0 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -36,11 +36,16 @@ RSpec.describe 'Dashboard Merge Requests', feature_category: :code_review_workfl
end
it 'shows projects only with merge requests feature enabled', :js do
- click_button 'Toggle project select'
+ click_button 'Select project to create merge request'
+ wait_for_requests
- page.within('.select2-results') do
+ page.within('[data-testid="new-resource-dropdown"]') do
expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_disabled_merge_requests.full_name)
+
+ find_button(project.full_name).click
+
+ expect(page).to have_link("New merge request in #{project.name}")
end
end
end
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index a9f23f90bb1..3b197bbf009 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -37,19 +37,13 @@ RSpec.describe 'Dashboard > Milestones', feature_category: :team_planning do
describe 'new milestones dropdown', :js do
it 'takes user to a new milestone page', :js do
- click_button 'Toggle project select'
+ click_button 'Select project to create milestone'
- page.within('.select2-results') do
- first('.select2-result-label').click
+ page.within('[data-testid="new-resource-dropdown"]') do
+ click_button group.name
+ click_link "New milestone in #{group.name}"
end
- a_el = find('.js-new-project-item-link')
-
- expect(a_el).to have_content('New Milestone in ')
- expect(a_el).to have_no_content('New New Milestone in ')
-
- a_el.click
-
expect(page).to have_current_path(new_group_milestone_path(group), ignore_query: true)
end
end
diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb
index 5bf1566fa31..c4dc78bf45b 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, feature_category: :users do
+RSpec.describe 'Project member activity', :js, feature_category: :user_profile do
let(:user) { create(:user) }
let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) }
diff --git a/spec/features/dashboard/root_explore_spec.rb b/spec/features/dashboard/root_explore_spec.rb
index c0d1f0de1f5..a232ebec68e 100644
--- a/spec/features/dashboard/root_explore_spec.rb
+++ b/spec/features/dashboard/root_explore_spec.rb
@@ -39,17 +39,5 @@ RSpec.describe 'Root explore', feature_category: :not_owned do
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/explore/topics_spec.rb b/spec/features/explore/topics_spec.rb
index b5787a2dba8..dcccaea8c80 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', feature_category: :users do
+RSpec.describe 'Explore Topics', feature_category: :user_profile 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 f54a51c9ac9..14fddf5d84c 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', feature_category: :users do
+RSpec.describe 'User explores projects', feature_category: :user_profile do
context 'when some projects exist' do
let_it_be(:archived_project) { create(:project, :archived) }
let_it_be(:internal_project) { create(:project, :internal) }
diff --git a/spec/features/file_uploads/user_avatar_spec.rb b/spec/features/file_uploads/user_avatar_spec.rb
index 06501e09866..062c47d5310 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, feature_category: :users do
+RSpec.describe 'Upload a user avatar', :js, feature_category: :user_profile do
let_it_be(:user, reload: true) { create(:user) }
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index 11ec38f637b..c451a97bed5 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:project) { create(:project_empty_repo, group: group) }
before do
+ stub_feature_flags(apollo_boards: false)
+
group.add_maintainer(user)
sign_in(user)
@@ -60,6 +62,8 @@ RSpec.describe 'Group Boards', feature_category: :team_planning do
let_it_be(:issue2) { create(:issue, title: 'issue2', project: project2) }
before do
+ stub_feature_flags(apollo_boards: false)
+
project1.add_guest(user)
project2.add_reporter(user)
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 3e565dd8eab..d876a5804bd 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, feature_category: :users do
+RSpec.describe 'User Cluster', :js, feature_category: :user_profile do
include GoogleApi::CloudPlatformHelpers
let(:group) { create(:group) }
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index a37c40f50e0..e123e223ae5 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -98,13 +98,9 @@ RSpec.describe 'Group empty states', feature_category: :subgroups do
end
it "the new #{issuable_name} button opens a project dropdown" do
- click_button 'Toggle project select'
+ click_button "Select project to create #{issuable_name}"
- if issuable == :issue
- expect(page).to have_button project.name
- else
- expect(page).to have_selector('.ajax-project-dropdown')
- end
+ expect(page).to have_button project.name
end
end
end
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index ab53ef7c470..ae757e04716 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -203,15 +203,24 @@ RSpec.describe "Group Runners", feature_category: :runner_fleet do
end
describe "Group runner show page", :js do
- let!(:group_runner) do
+ let_it_be(:group_runner) do
create(:ci_runner, :group, groups: [group], description: 'runner-foo')
end
- it 'user views runner details' do
+ let_it_be(:group_runner_job) { create(:ci_build, runner: group_runner) }
+
+ before do
visit group_runner_path(group, group_runner)
+ end
+ it 'user views runner details' do
expect(page).to have_content "#{s_('Runners|Description')} runner-foo"
end
+
+ it_behaves_like 'shows runner jobs tab' do
+ let(:job_count) { '1' }
+ let(:job) { group_runner_job }
+ end
end
describe "Group runner edit page", :js do
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index fe1b0909c06..5510e73ef0f 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe 'Edit group settings', feature_category: :subgroups do
page.within(confirm_modal) do
expect(page).to have_text "You are going to transfer #{selected_group.name} to another namespace. Are you ABSOLUTELY sure?"
- fill_in 'confirm_name_input', with: selected_group.name
+ fill_in 'confirm_name_input', with: selected_group.full_path
click_button 'Confirm'
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 8a3401d0572..bbb7d322b9a 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -77,9 +77,9 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
end
it 'shows projects only with merge requests feature enabled', :js do
- find('.js-new-project-item-link').click
+ click_button 'Select project to create merge request'
- page.within('.select2-results') do
+ page.within('[data-testid="new-resource-dropdown"]') do
expect(page).to have_content(project.name_with_namespace)
expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace)
end
@@ -95,7 +95,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
visit path
expect(page).to have_selector('.empty-state')
- expect(page).to have_link('Select project to create merge request')
+ expect(page).to have_button('Select project to create merge request')
expect(page).to have_selector('.issues-filters')
end
@@ -105,7 +105,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
visit path
expect(page).to have_selector('.empty-state')
- expect(page).to have_link('Select project to create merge request')
+ expect(page).to have_button('Select project to create merge request')
expect(page).to have_selector('.issues-filters')
end
end
diff --git a/spec/features/groups/settings/packages_and_registries_spec.rb b/spec/features/groups/settings/packages_and_registries_spec.rb
index 60aad8452ce..80e2dcd5174 100644
--- a/spec/features/groups/settings/packages_and_registries_spec.rb
+++ b/spec/features/groups/settings/packages_and_registries_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Group Package and registry settings', feature_category: :package
it 'has a page title set' do
visit_settings_page
- expect(page).to have_title _('Package and registry settings')
+ expect(page).to have_title _('Packages and registries settings')
end
it 'sidebar menu is open' do
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index c0af6080d0f..5cab79b40cf 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Group show page', feature_category: :subgroups do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -37,6 +39,16 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
expect(page).to have_content('Collaborate with your team')
page.within(find('[data-testid="invite-members-banner"]')) do
+ click_button('Invite your colleagues')
+ end
+
+ page.within(invite_modal_selector) do
+ expect(page).to have_content("You're inviting members to the #{group.name} group")
+
+ click_button('Cancel')
+ end
+
+ page.within(find('[data-testid="invite-members-banner"]')) do
find('[data-testid="close-icon"]').click
end
@@ -73,7 +85,7 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
end
- context 'subgroups and projects empty state', :js do
+ context 'with subgroups and projects empty state', :js do
context 'when user has permissions to create new subgroups or projects' do
before do
group.add_owner(user)
@@ -82,22 +94,19 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
it 'shows `Create new subgroup` link' do
- expect(page).to have_link(
- s_('GroupsEmptyState|Create new subgroup'),
- href: new_group_path(parent_id: group.id, anchor: 'create-group-pane')
- )
+ link = new_group_path(parent_id: group.id, anchor: 'create-group-pane')
+
+ expect(page).to have_link(s_('GroupsEmptyState|Create new subgroup'), href: link)
end
it 'shows `Create new project` link' do
- expect(page).to have_link(
- s_('GroupsEmptyState|Create new project'),
- href: new_project_path(namespace_id: group.id)
- )
+ expect(page)
+ .to have_link(s_('GroupsEmptyState|Create new project'), href: new_project_path(namespace_id: group.id))
end
end
end
- context 'visibility warning popover' do
+ context 'with visibility warning popover' do
let_it_be(:public_project) { create(:project, :public) }
shared_examples 'it shows warning popover' do
@@ -145,23 +154,22 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
it 'does not show `Create new subgroup` link' do
- expect(page).not_to have_link(
- s_('GroupsEmptyState|Create new subgroup'),
- href: new_group_path(parent_id: group.id)
- )
+ expect(page)
+ .not_to have_link(s_('GroupsEmptyState|Create new subgroup'), href: new_group_path(parent_id: group.id))
end
it 'does not show `Create new project` link' do
- expect(page).not_to have_link(
- s_('GroupsEmptyState|Create new project'),
- href: new_project_path(namespace_id: group.id)
- )
+ expect(page)
+ .not_to have_link(s_('GroupsEmptyState|Create new project'), href: new_project_path(namespace_id: group.id))
end
it 'shows empty state' do
+ content = s_('GroupsEmptyState|You do not have necessary permissions to create a subgroup ' \
+ 'or project in this group. Please contact an owner of this group to create a ' \
+ 'new subgroup or project.')
+
expect(page).to have_content(s_('GroupsEmptyState|No subgroups or projects.'))
- expect(page).to have_content(s_('GroupsEmptyState|You do not have necessary permissions to create a subgroup' \
- ' or project in this group. Please contact an owner of this group to create a new subgroup or project.'))
+ expect(page).to have_content(content)
end
end
end
@@ -198,7 +206,7 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
end
- context 'subgroup support' do
+ context 'with subgroup support' do
let_it_be(:restricted_group) do
create(:group, subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
end
@@ -255,7 +263,7 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
end
- context 'notification button', :js do
+ context 'for notification button', :js do
before do
group.add_maintainer(user)
sign_in(user)
@@ -276,7 +284,7 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
end
end
- context 'page og:description' do
+ context 'for page og:description' do
before do
group.update!(description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)')
group.add_maintainer(user)
@@ -287,7 +295,7 @@ RSpec.describe 'Group show page', feature_category: :subgroups do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
end
- context 'structured schema markup' do
+ context 'for structured schema markup' do
let_it_be(:group) { create(:group, :public, :with_avatar, description: 'foo') }
let_it_be(:subgroup) { create(:group, :public, :with_avatar, parent: group, description: 'bar') }
let_it_be_with_reload(:project) { create(:project, :public, :with_avatar, namespace: group, description: 'foo') }
diff --git a/spec/features/ide/clientside_preview_csp_spec.rb b/spec/features/ide/clientside_preview_csp_spec.rb
deleted file mode 100644
index 04427a5c294..00000000000
--- a/spec/features/ide/clientside_preview_csp_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'IDE Clientside Preview CSP', feature_category: :web_ide do
- let_it_be(:user) { create(:user) }
-
- shared_context 'disable feature' do
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: false)
- end
- end
-
- it_behaves_like 'setting CSP', 'frame-src' do
- let(:allowlisted_url) { 'https://sandbox.gitlab-static.test' }
- let(:extended_controller_class) { IdeController }
-
- subject do
- visit ide_path
-
- response_headers['Content-Security-Policy']
- end
-
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: true)
- stub_application_setting(web_ide_clientside_preview_bundler_url: allowlisted_url)
-
- sign_in(user)
- end
- end
-end
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index e1167285464..709919d0196 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -3,11 +3,16 @@
require 'spec_helper'
RSpec.describe 'Incident details', :js, feature_category: :incident_management do
+ include MergeRequestDiffHelpers
+
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, author: developer, description: 'description') }
let_it_be(:issue) { create(:issue, project: project, author: developer, description: 'Issue description') }
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: incident) }
+ let_it_be(:confidential_incident) do
+ create(:incident, confidential: true, project: project, author: developer, description: 'Confidential')
+ end
before_all do
project.add_developer(developer)
@@ -19,7 +24,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
context 'when a developer+ displays the incident' do
before do
- visit project_issues_incident_path(project, incident)
+ visit incident_project_issues_path(project, incident)
wait_for_requests
end
@@ -98,7 +103,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
page.within('[data-testid="issuable-form"]') do
click_button 'Issue'
- click_button 'Incident'
+ find('[data-testid="issue-type-list-item"]', text: 'Incident').click
click_button 'Save changes'
wait_for_requests
@@ -108,7 +113,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
- visit project_issues_incident_path(project, incident)
+ visit incident_project_issues_path(project, incident)
wait_for_requests
project_path = "/#{project.full_path}"
@@ -117,7 +122,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
page.within('[data-testid="issuable-form"]') do
click_button 'Incident'
- click_button 'Issue'
+ find('[data-testid="issue-type-list-item"]', text: 'Issue').click
click_button 'Save changes'
wait_for_requests
@@ -125,4 +130,12 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
expect(page).to have_current_path("#{project_path}/-/issues/#{incident.iid}")
end
end
+
+ it 'displays the confidential badge on the sticky header when the incident is confidential' do
+ visit incident_project_issues_path(project, confidential_incident)
+ wait_for_requests
+
+ sticky_header = find_by_scrolling('[data-testid=issue-sticky-header]')
+ expect(sticky_header.find('[data-testid=confidential]')).to be_present
+ end
end
diff --git a/spec/features/incidents/incident_timeline_events_spec.rb b/spec/features/incidents/incident_timeline_events_spec.rb
index 3a73ea50247..7404ac64cc9 100644
--- a/spec/features/incidents/incident_timeline_events_spec.rb
+++ b/spec/features/incidents/incident_timeline_events_spec.rb
@@ -3,99 +3,98 @@
require 'spec_helper'
RSpec.describe 'Incident timeline events', :js, feature_category: :incident_management do
+ include ListboxHelpers
+
let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:incident) { create(:incident, project: project) }
- before_all do
- project.add_developer(developer)
- end
-
- before do
- sign_in(developer)
-
- visit project_issues_incident_path(project, incident)
- wait_for_requests
- click_link s_('Incident|Timeline')
- end
-
- context 'when add event is clicked' do
- it 'submits event data when save is clicked' do
- click_button s_('Incident|Add new timeline event')
-
- expect(page).to have_selector('.common-note-form')
-
- fill_in _('Description'), with: 'Event note goes here'
- fill_in 'timeline-input-hours', with: '07'
- fill_in 'timeline-input-minutes', with: '25'
-
- click_button _('Save')
+ shared_examples 'add, edit, and delete timeline events' do
+ it 'submits event data on save' do
+ # Add event
+ click_button(s_('Incident|Add new timeline event'))
+ complete_form('Event note goes here', '07', '25')
expect(page).to have_selector('.incident-timeline-events')
-
page.within '.timeline-event-note' do
expect(page).to have_content('Event note goes here')
expect(page).to have_content('07:25')
end
- end
- end
- context 'when edit is clicked' do
- before do
- click_button 'Add new timeline event'
- fill_in 'Description', with: 'Event note to edit'
- click_button _('Save')
- end
+ # Edit event
+ trigger_dropdown_action(_('Edit'))
+ complete_form('Edited event note goes here', '08', '30')
- it 'shows the confirmation modal and edits the event' do
- click_button _('More actions')
+ page.within '.timeline-event-note' do
+ expect(page).to have_content('Edited event note goes here')
+ expect(page).to have_content('08:30')
+ end
- page.within '.gl-dropdown-contents' do
- expect(page).to have_content(_('Edit'))
- page.find('.gl-dropdown-item-text-primary', text: _('Edit')).click
+ # Delete event
+ trigger_dropdown_action(_('Delete'))
+
+ page.within '.modal' do
+ expect(page).to have_content(s_('Incident|Delete event'))
end
- expect(page).to have_selector('.common-note-form')
+ click_button s_('Incident|Delete event')
+ wait_for_requests
+
+ expect(page).to have_content(s_('Incident|No timeline items have been added yet.'))
+ end
+
+ it 'submits event data on save with feature flag on' do
+ stub_feature_flags(incident_event_tags: true)
- fill_in _('Description'), with: 'Event note goes here'
- fill_in 'timeline-input-hours', with: '07'
- fill_in 'timeline-input-minutes', with: '25'
+ # Add event
+ click_button(s_('Incident|Add new timeline event'))
- click_button _('Save')
+ select_from_listbox('Start time', from: 'Select tags')
- wait_for_requests
+ complete_form('Event note goes here', '07', '25')
+ expect(page).to have_selector('.incident-timeline-events')
page.within '.timeline-event-note' do
expect(page).to have_content('Event note goes here')
expect(page).to have_content('07:25')
+ expect(page).to have_content('Start time')
end
- end
- end
- context 'when delete is clicked' do
- before do
- click_button s_('Incident|Add new timeline event')
- fill_in _('Description'), with: 'Event note to delete'
- click_button _('Save')
- end
+ # Edit event
+ trigger_dropdown_action(_('Edit'))
- it 'shows the confirmation modal and deletes the event' do
- click_button _('More actions')
+ select_from_listbox('Start time', from: 'Start time')
- page.within '.gl-dropdown-contents' do
- expect(page).to have_content(_('Delete'))
- page.find('.gl-dropdown-item-text-primary', text: 'Delete').click
- end
+ complete_form('Edited event note goes here', '08', '30')
- page.within '.modal' do
- expect(page).to have_content(s_('Incident|Delete event'))
+ page.within '.timeline-event-note' do
+ expect(page).to have_content('Edited event note goes here')
+ expect(page).to have_content('08:30')
+ expect(page).not_to have_content('Start time')
end
+ end
- click_button s_('Incident|Delete event')
+ private
+ def complete_form(title, hours, minutes)
+ fill_in _('Description'), with: title
+ fill_in 'timeline-input-hours', with: hours
+ fill_in 'timeline-input-minutes', with: minutes
+
+ click_button _('Save')
wait_for_requests
+ end
- expect(page).to have_content(s_('Incident|No timeline items have been added yet.'))
+ def trigger_dropdown_action(text)
+ click_button _('More actions')
+
+ page.within '.gl-dropdown-contents' do
+ page.find('.gl-dropdown-item', text: text).click
+ end
end
end
+
+ it_behaves_like 'for each incident details route',
+ 'add, edit, and delete timeline events',
+ tab_text: s_('Incident|Timeline')
end
diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb
index 49041d187dd..0265960fce7 100644
--- a/spec/features/incidents/user_views_incident_spec.rb
+++ b/spec/features/incidents/user_views_incident_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe "User views incident", feature_category: :incident_management do
before do
sign_in(user)
- visit(project_issues_incident_path(project, incident))
+ visit(incident_project_issues_path(project, incident))
end
specify do
@@ -75,7 +75,7 @@ RSpec.describe "User views incident", feature_category: :incident_management do
describe 'user status' do
context 'when showing status of the author of the incident' do
- subject { visit(project_issues_incident_path(project, incident)) }
+ subject { visit(incident_project_issues_path(project, incident)) }
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index 0190111b2f0..06387c14ee2 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do
before do
project.add_developer(user)
+
sign_in(user)
end
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index b5362267309..9045124cc8c 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
+ include ListboxHelpers
+
let(:project) { create(:project, :public) }
let(:first_created_issuable) { issuables.order_created_asc.first }
@@ -94,8 +96,7 @@ RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
it 'supports sorting in asc and desc order' do
visit_merge_requests_with_state(project, 'open')
- click_button('Created date')
- find('.dropdown-item', text: 'Updated date').click
+ select_from_listbox('Updated date', from: 'Created date')
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
@@ -209,7 +210,7 @@ RSpec.describe 'Sort Issuable List', feature_category: :team_planning do
end
def selected_sort_order
- find('.filter-dropdown-container .dropdown button').text.downcase
+ find('.filter-dropdown-container .gl-new-dropdown button').text.downcase
end
def visit_merge_requests_with_state(project, state)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 2898c97c2e9..585740f7782 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -353,14 +353,14 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
expect(find('#issue_description').value).to match('description from query parameter')
end
- it 'fills the description from the issuable_template query parameter' do
+ it 'fills the description from the issuable_template query parameter', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/388728' do
visit new_project_issue_path(project, issuable_template: 'test_template')
wait_for_requests
expect(find('#issue_description').value).to match('description from template')
end
- it 'fills the description from the issuable_template and issue[description] query parameters' do
+ it 'fills the description from the issuable_template and issue[description] query parameters', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/388728' do
visit new_project_issue_path(project, issuable_template: 'test_template', issue: { description: 'description from query parameter' })
wait_for_requests
diff --git a/spec/features/issues/incident_issue_spec.rb b/spec/features/issues/incident_issue_spec.rb
index 2fba1ca9141..41bbd79202f 100644
--- a/spec/features/issues/incident_issue_spec.rb
+++ b/spec/features/issues/incident_issue_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Incident Detail', :js, feature_category: :team_planning do
project.add_developer(user)
sign_in(user)
- visit project_issues_incident_path(project, incident)
+ visit incident_project_issues_path(project, incident)
wait_for_requests
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 44e9bbad1ba..20a69c61871 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
page.within('[data-testid="issuable-form"]') do
update_type_select('Issue', 'Incident')
- expect(page).to have_current_path(project_issues_incident_path(project, issue))
+ expect(page).to have_current_path(incident_project_issues_path(project, issue))
end
end
end
@@ -170,7 +170,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
def update_type_select(from, to)
click_button from
- click_button to
+ find('[data-testid="issue-type-list-item"]', text: to).click
click_button 'Save changes'
wait_for_requests
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index fa72acad8c6..686074f7412 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include MobileHelpers
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
@@ -120,11 +121,13 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
expect(page).to have_link('Invite members')
expect(page).to have_selector('[data-track-action="click_invite_members"]')
expect(page).to have_selector('[data-track-label="edit_assignee"]')
- end
- click_link 'Invite members'
+ click_link 'Invite members'
+ end
- expect(page).to have_content("You're inviting members to the")
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
@@ -208,7 +211,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
visit_issue(project, issue)
end
- context 'sidebar', :js do
+ context 'for sidebar', :js do
it 'changes size when the screen size is smaller' do
sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
# Resize the window
@@ -227,25 +230,25 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
end
end
- context 'editing issue milestone', :js do
+ context 'for editing issue milestone', :js do
it_behaves_like 'milestone sidebar widget'
end
- context 'editing issue due date', :js do
+ context 'for editing issue due date', :js do
it_behaves_like 'date sidebar widget'
end
- context 'editing issue labels', :js do
+ context 'for editing issue labels', :js do
it_behaves_like 'labels sidebar widget'
end
- context 'escalation status', :js do
+ context 'for escalation status', :js do
it 'is not available for default issue type' do
expect(page).not_to have_selector('.block.escalation-status')
end
end
- context 'interacting with collapsed sidebar', :js do
+ context 'when interacting with collapsed sidebar', :js do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
confidentiality_sidebar_block = '.block.confidentiality'
@@ -300,7 +303,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
expect(page).not_to have_selector('.block.labels .js-sidebar-dropdown-toggle')
end
- context 'sidebar', :js do
+ context 'for sidebar', :js do
it 'finds issue copy forwarding email' do
expect(
find('[data-testid="copy-forward-email"]').text
@@ -308,7 +311,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
end
end
- context 'interacting with collapsed sidebar', :js do
+ context 'when interacting with collapsed sidebar', :js do
collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
lock_sidebar_block = '.block.lock'
@@ -334,7 +337,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
end
context 'when not signed in' do
- context 'sidebar', :js do
+ context 'for sidebar', :js do
before do
visit_issue(project, issue)
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 72c6e288168..ea68f2266b3 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
end
it 'moving issue to another project not allowed' do
- expect(page).to have_no_selector('.js-sidebar-move-issue-block')
+ expect(page).to have_no_selector('.js-issuable-move-block')
end
end
@@ -42,10 +42,10 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
end
it 'moving issue to another project', :js do
- find('.js-move-issue').click
+ click_button _('Move issue')
wait_for_requests
- all('.js-move-issue-dropdown-item')[0].click
- find('.js-move-issue-confirmation-button').click
+ all('.gl-dropdown-item')[0].click
+ click_button _('Move')
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}")
@@ -56,11 +56,11 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
it 'searching project dropdown', :js do
new_project_search.add_reporter(user)
- find('.js-move-issue').click
+ click_button _('Move issue')
wait_for_requests
- page.within '.js-sidebar-move-issue-block' do
- fill_in('sidebar-move-issue-dropdown-search', with: new_project_search.name)
+ page.within '.js-issuable-move-block' do
+ fill_in(_('Search project'), with: new_project_search.name)
expect(page).to have_content(new_project_search.name)
expect(page).not_to have_content(new_project.name)
@@ -76,10 +76,10 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
end
it 'browsing projects in projects select' do
- find('.js-move-issue').click
+ click_button _('Move issue')
wait_for_requests
- page.within '.js-sidebar-move-issue-block' do
+ page.within '.js-issuable-move-block' do
expect(page).to have_content new_project.full_name
end
end
@@ -115,10 +115,10 @@ RSpec.describe 'issue move to another project', feature_category: :team_planning
visit issue_path(service_desk_issue)
- find('.js-move-issue').click
+ click_button _('Move issue')
wait_for_requests
- find('.js-move-issue-dropdown-item', text: project_title).click
- find('.js-move-issue-confirmation-button').click
+ find('.gl-dropdown-item', text: project_title).click
+ click_button _('Move')
end
it 'shows an alert after being moved' do
diff --git a/spec/features/issues/spam_akismet_issue_creation_spec.rb b/spec/features/issues/spam_akismet_issue_creation_spec.rb
index 7c62f141105..176c26c6d8a 100644
--- a/spec/features/issues/spam_akismet_issue_creation_spec.rb
+++ b/spec/features/issues/spam_akismet_issue_creation_spec.rb
@@ -108,20 +108,20 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
end
end
- shared_context 'when allow_possible_spam feature flag is true' do
+ shared_context 'when allow_possible_spam application setting is true' do
before do
- stub_feature_flags(allow_possible_spam: true)
+ stub_application_setting(allow_possible_spam: true)
end
end
- shared_context 'when allow_possible_spam feature flag is false' do
+ shared_context 'when allow_possible_spam application setting is false' do
before do
- stub_feature_flags(allow_possible_spam: false)
+ stub_application_setting(allow_possible_spam: false)
end
end
describe 'spam handling' do
- # verdict, spam_flagged, captcha_enabled, allow_possible_spam_flag, creates_spam_log
+ # verdict, spam_flagged, captcha_enabled, allow_possible_spam, creates_spam_log
# TODO: Add example for BLOCK_USER verdict when we add support for testing SpamCheck - see https://gitlab.com/groups/gitlab-org/-/epics/5527#lacking-coverage-for-spamcheck-vs-akismet
# DISALLOW, true, false, false, true
# CONDITIONAL_ALLOW, true, true, false, true
@@ -133,7 +133,7 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
context 'DISALLOW: spam_flagged=true, captcha_enabled=true, allow_possible_spam=true' do
include_context 'when spammable is identified as possible spam'
include_context 'when CAPTCHA is enabled'
- include_context 'when allow_possible_spam feature flag is true'
+ include_context 'when allow_possible_spam application setting is true'
it_behaves_like 'allows issue creation without CAPTCHA'
it_behaves_like 'creates a spam_log record'
@@ -142,7 +142,7 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
context 'CONDITIONAL_ALLOW: spam_flagged=true, captcha_enabled=true, allow_possible_spam=false' do
include_context 'when spammable is identified as possible spam'
include_context 'when CAPTCHA is enabled'
- include_context 'when allow_possible_spam feature flag is false'
+ include_context 'when allow_possible_spam application setting is false'
it_behaves_like 'allows issue creation with CAPTCHA'
it_behaves_like 'creates a spam_log record'
@@ -151,7 +151,7 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
context 'OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM: spam_flagged=true, captcha_enabled=true, allow_possible_spam=true' do
include_context 'when spammable is identified as possible spam'
include_context 'when CAPTCHA is enabled'
- include_context 'when allow_possible_spam feature flag is true'
+ include_context 'when allow_possible_spam application setting is true'
it_behaves_like 'allows issue creation without CAPTCHA'
it_behaves_like 'creates a spam_log record'
@@ -160,7 +160,7 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
context 'OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM: spam_flagged=true, captcha_enabled=false, allow_possible_spam=true' do
include_context 'when spammable is identified as possible spam'
include_context 'when CAPTCHA is not enabled'
- include_context 'when allow_possible_spam feature flag is true'
+ include_context 'when allow_possible_spam application setting is true'
it_behaves_like 'allows issue creation without CAPTCHA'
it_behaves_like 'creates a spam_log record'
@@ -169,7 +169,7 @@ RSpec.describe 'Spam detection on issue creation', :js, feature_category: :team_
context 'ALLOW: spam_flagged=false, captcha_enabled=true, allow_possible_spam=false' do
include_context 'when spammable is not identified as possible spam'
include_context 'when CAPTCHA is not enabled'
- include_context 'when allow_possible_spam feature flag is false'
+ include_context 'when allow_possible_spam application setting is false'
it_behaves_like 'allows issue creation without CAPTCHA'
it_behaves_like 'does not create a spam_log record'
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index fc48bc4baf9..5696bde4069 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
click_button 'Edit issues'
check 'Select all'
click_update_assignee_button
- click_link user.username
+ click_button user.username
click_update_issues_button
@@ -64,7 +64,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
click_button 'Edit issues'
check 'Select all'
click_update_assignee_button
- click_link 'Unassigned'
+ click_button 'Unassigned'
click_update_issues_button
expect(find('.issue:first-of-type')).not_to have_link "Assigned to #{user.name}"
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index df039493cec..c5d0791dc57 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -157,15 +157,10 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
end
end
- context 'form filled by URL parameters' do
+ context 'form filled by URL parameters', :use_null_store_as_repository_cache do
let(:project) { create(:project, :public, :repository) }
before do
- # With multistore feature flags enabled (using an actual Redis store instead of NullStore),
- # it somehow writes an invalid content to Redis and the specs would fail.
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
-
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 19b2633969d..bf2af918f39 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
visit edit_project_issue_path(project, issue)
end
- it "previews content" do
+ it "previews content", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391757' do
form = first(".gfm-form")
page.within(form) do
@@ -433,7 +433,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
issue.save!
end
- it 'shows milestone text' do
+ it 'shows milestone text', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/389287' do
sign_out(:user)
sign_in(guest)
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index 8cf07f2ade2..c90c0d2dda9 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -20,8 +20,8 @@ RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integ
sign_in(alice)
end
- def within_dropdown(&block)
- within('.dropdown-menu', &block)
+ def within_project_listbox(&block)
+ within('#project-select', &block)
end
it 'select project and branch and submit the form' do
@@ -35,16 +35,16 @@ RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integ
click_on 'Select a project'
- within_dropdown do
- expect(page).to have_text('Alice / foo')
- expect(page).to have_text('Alice / bar')
- expect(page).not_to have_text('Bob /')
+ within_project_listbox do
+ expect_listbox_item('Alice / foo')
+ expect_listbox_item('Alice / bar')
+ expect_no_listbox_item('Bob /')
fill_in 'Search', with: 'foo'
- expect(page).not_to have_text('Alice / bar')
+ expect_no_listbox_item('Alice / bar')
- find('span', text: 'Alice / foo', match: :first).click
+ select_listbox_item('Alice / foo')
end
expect(page).to have_field('Branch name', with: 'ACME-123-my-issue-title')
@@ -61,9 +61,9 @@ RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integ
find('span', text: 'Alice / foo', match: :first).click
- within_dropdown do
+ within_project_listbox do
fill_in 'Search', with: ''
- find('span', text: 'Alice / bar', match: :first).click
+ select_listbox_item('Alice / bar')
end
click_on 'master'
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index 132a03877f8..6e62aa892bb 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -296,6 +296,11 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_p
expect(img.attr('width')).to eq('75%')
expect(img.attr('height')).to eq('100')
+
+ vid = doc.at_css('video[data-title="Sized Video"]')
+
+ expect(vid.attr('width')).to eq('75%')
+ expect(vid.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 0c77bd2a8ff..25459494a0c 100644
--- a/spec/features/markdown/math_spec.rb
+++ b/spec/features/markdown/math_spec.rb
@@ -64,7 +64,82 @@ RSpec.describe 'Math rendering', :js, feature_category: :team_planning do
visit project_issue_path(project, issue)
page.within '.description > .md' do
- expect(page).to have_selector('.js-lazy-render-math')
+ expect(page).to have_selector('.js-lazy-render-math-container', text: /math block exceeds 1000 characters/)
+ end
+ end
+
+ it 'allows many expansions', :js do
+ description = <<~MATH
+ ```math
+ #{'\\mod e ' * 100}
+ ```
+ MATH
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ page.within '.description > .md' do
+ expect(page).not_to have_selector('.katex-error')
+ end
+ end
+
+ it 'shows error message when too many expansions', :js do
+ description = <<~MATH
+ ```math
+ #{'\\mod e ' * 150}
+ ```
+ MATH
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ page.within '.description > .md' do
+ click_button 'Display anyway'
+
+ expect(page).to have_selector('.katex-error', text: /Too many expansions/)
+ end
+ end
+
+ it 'shows error message when other errors are generated', :js do
+ description = <<~MATH
+ ```math
+ \\unknown
+ ```
+ MATH
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ page.within '.description > .md' do
+ expect(page).to have_selector('.katex-error',
+ text: /There was an error rendering this math block. KaTeX parse error/)
+ end
+ end
+
+ it 'escapes HTML in error', :js do
+ description = <<~MATH
+ ```math
+ \\unknown <script>
+ ```
+ MATH
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ page.within '.description > .md' do
+ expect(page).to have_selector('.katex-error', text: /&amp;lt;script&amp;gt;/)
end
end
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index 736c986d0fe..ddbcb04fa80 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -210,7 +210,8 @@ RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_re
page.find('.js-diff-comment-avatar').click
end
- it 'publishes comment right away and unresolves the thread' do
+ it 'publishes comment right away and unresolves the thread',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337931' do
expect(active_discussion.resolved?).to eq(true)
write_reply_to_discussion(button_text: 'Add comment now', unresolve: true)
@@ -220,7 +221,8 @@ RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_re
end
end
- it 'publishes review and unresolves the thread' do
+ it 'publishes review and unresolves the thread',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337931' do
expect(active_discussion.resolved?).to eq(true)
wait_for_requests
@@ -252,10 +254,10 @@ RSpec.describe 'Merge request > Batch comments', :js, feature_category: :code_re
wait_for_requests
end
- def write_diff_comment(**params)
+ def write_diff_comment(...)
click_diff_line(find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']"))
- write_comment(**params)
+ write_comment(...)
end
def write_parallel_comment(line, **params)
diff --git a/spec/features/merge_request/user_can_see_draft_toggle_spec.rb b/spec/features/merge_request/user_can_see_draft_toggle_spec.rb
new file mode 100644
index 00000000000..ac7b5cdab04
--- /dev/null
+++ b/spec/features/merge_request/user_can_see_draft_toggle_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User sees draft toggle', feature_category: :code_review_workflow do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'with draft commits' do
+ it 'shows the draft toggle' do
+ visit project_new_merge_request_path(
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'wip',
+ target_branch: 'master'
+ })
+
+ expect(page).to have_css('input[type="checkbox"].js-toggle-draft', count: 1)
+ expect(page).to have_text('Mark as draft')
+ expect(page).to have_text('Drafts cannot be merged until marked ready.')
+ end
+ end
+
+ context 'without draft commits' do
+ it 'shows the draft toggle' do
+ visit project_new_merge_request_path(
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+
+ expect(page).to have_css('input[type="checkbox"].js-toggle-draft', count: 1)
+ expect(page).to have_text('Mark as draft')
+ expect(page).to have_text('Drafts cannot be merged until marked ready.')
+ end
+ end
+end
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 1717069a259..97b423f2cc2 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'User creates a merge request', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
+ include ListboxHelpers
shared_examples 'creates a merge request' do
specify do
@@ -73,19 +74,9 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
expect(page).to have_content('You must select source and target branch')
- first('.js-source-project').click
- first('.dropdown-source-project a', text: forked_project.full_path)
-
- first('.js-target-project').click
- first('.dropdown-target-project li', text: project.full_path)
-
- first('.js-source-branch').click
-
- wait_for_requests
-
- source_branch = 'fix'
-
- first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click
+ select_project('.js-source-project', forked_project)
+ select_project('.js-target-project', project)
+ select_branch('.js-source-branch', 'fix')
click_button('Compare branches and continue')
@@ -107,7 +98,7 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
click_button('Create merge request')
- expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
+ expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:fix into master")
end
end
end
@@ -153,12 +144,27 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
private
def compare_source_and_target(source_branch, target_branch)
- find('.js-source-branch').click
- click_link(source_branch)
-
- find('.js-target-branch').click
- click_link(target_branch)
+ select_branch('.js-source-branch', source_branch)
+ select_branch('.js-target-branch', target_branch)
click_button('Compare branches')
end
+
+ def select_project(selector, project)
+ first(selector).click
+
+ wait_for_requests
+
+ find('.gl-listbox-search-input').set(project.full_path)
+ select_listbox_item(project.full_path)
+ end
+
+ def select_branch(selector, branch)
+ first(selector).click
+
+ wait_for_requests
+
+ find('.gl-listbox-search-input').set(branch)
+ select_listbox_item(branch, exact_text: true)
+ end
end
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index 523027582b3..6ee20a08a47 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -59,9 +59,9 @@ RSpec.describe 'Merge request > User creates MR', feature_category: :code_review
it 'filters source project' do
find('.js-source-project').click
- find('.dropdown-source-project input').set('source')
+ find('.gl-listbox-search-input').set('source')
- expect(find('.dropdown-source-project .dropdown-content')).not_to have_content(source_project.name)
+ expect(first('.merge-request-select .gl-new-dropdown-panel')).not_to have_content(source_project.name)
end
end
end
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 60631027d9d..cf5024ad59e 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_category: :code_review_workflow do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
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) }
@@ -15,11 +17,17 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
# DOM finders to simplify and improve readability
let(:sidebar_assignee_block) { page.find('.js-issuable-sidebar .assignee') }
- let(:sidebar_assignee_avatar_link) { sidebar_assignee_block.find_all('a').find { |a| a['href'].include? assignee.username } }
+ let(:sidebar_assignee_avatar_link) do
+ sidebar_assignee_block.find_all('a').find { |a| a['href'].include? assignee.username }
+ end
+
let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['title'] || '' }
context 'when GraphQL assignees widget feature flag is disabled' do
- let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]") }
+ let(:sidebar_assignee_dropdown_item) do
+ sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]")
+ end
+
let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item.find('a')['data-title'] || '' }
before do
@@ -156,11 +164,13 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js, feature_cate
expect(page).to have_link('Invite members')
expect(page).to have_selector('[data-track-action="click_invite_members"]')
expect(page).to have_selector('[data-track-label="edit_assignee"]')
- end
- click_link 'Invite members'
+ click_link 'Invite members'
+ end
- expect(page).to have_content("You're inviting members to the")
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb
index 79c166434aa..d47968ebc6b 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js, feature_category
end
end
- expect(find('.media-body h4')).to have_content('Merging!')
+ expect(find('[data-testid="merging-state"]')).to have_content('Merging!')
wait_for_requests
end
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 c73ba1bdbe5..cdc00017ab3 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
expect(page).not_to have_button('Merge')
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.')
+ expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
end
end
@@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea
wait_for_requests
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.')
+ expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.')
end
end
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 8a19a72f6ae..01cc6bd5167 100644
--- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb
+++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co
it 'retains merge request data after clicking Resolve WIP status' do
expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}")
- expect(page).to have_content "Merge blocked: merge request must be marked as ready. It's still marked as draft."
+ expect(page).to have_content "Merge blocked: Select Mark as ready to remove it from Draft status."
page.within('.mr-state-widget') do
click_button('Mark as ready')
@@ -45,7 +45,7 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co
# merge request widget refreshes, which masks missing elements
# that should already be present.
expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}")
- expect(page).not_to have_content("Merge blocked: merge request must be marked as ready. It's still marked as draft.")
+ expect(page).not_to have_content("Merge blocked: Select Mark as ready to remove it from Draft status.")
end
end
end
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index daeeaa1bd88..12fdcf4859e 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Merge request > User sees diff', :js, feature_category: :code_re
visit "#{diffs_project_merge_request_path(project, merge_request)}#{fragment}"
end
- it 'shows expanded note' do
+ it 'shows expanded note', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391239' do
expect(page).to have_selector(fragment, visible: true)
end
end
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
index 9d3046a9a72..6e6c2cddfbf 100644
--- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
expect(page).to have_selector(second_discussion_selector, obscured: false)
end
- it 'cycles back to the first thread' do
+ it 'cycles back to the first thread', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391604' do
goto_next_thread
goto_next_thread
goto_next_thread
@@ -92,7 +92,8 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_
page.execute_script("window.scrollTo(0,0)")
end
- it 'excludes resolved threads during navigation' do
+ it 'excludes resolved threads during navigation',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383687' do
goto_next_thread
goto_next_thread
goto_next_thread
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 0297bb5b935..acf2893b513 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -18,7 +18,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
end
before do
- stub_feature_flags(refactor_security_extension: false)
project.add_maintainer(user)
project_only_mwps.add_maintainer(user)
sign_in(user)
@@ -90,12 +89,12 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_button 'master'
end
- page.within("#{modal_selector} .dropdown-menu") do
- find('[data-testid="dropdown-search-box"]').set('')
+ page.within("#{modal_selector} [data-testid=\"base-dropdown-menu\"]") do
+ fill_in 'Search branches', with: ''
wait_for_requests
- expect(page.all('[data-testid="dropdown-item"]').size).to be > 1
+ expect(page).to have_selector('[data-testid="listbox-item-master"]', visible: true)
end
end
end
@@ -149,7 +148,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_button 'Merge unverified changes'
end
- expect(find('.media-body h4')).to have_content('Merging!')
+ expect(find('[data-testid="merging-state"]')).to have_content('Merging!')
end
end
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
deleted file mode 100644
index fdefe5ffb06..00000000000
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Merge request > User sees draft help message', feature_category: :code_review_workflow do
- let(:project) { create(:project, :public, :repository) }
- let(:user) { project.creator }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- context 'with draft commits' do
- it 'shows a specific draft hint' do
- visit project_new_merge_request_path(
- project,
- merge_request: {
- source_project_id: project.id,
- target_project_id: project.id,
- source_branch: 'wip',
- target_branch: 'master'
- })
-
- within_wip_explanation do
- expect(page).to have_text(
- 'It looks like you have some draft commits in this branch'
- )
- end
- end
- end
-
- context 'without draft commits' do
- it 'shows the regular draft message' do
- visit project_new_merge_request_path(
- project,
- merge_request: {
- source_project_id: project.id,
- target_project_id: project.id,
- source_branch: 'fix',
- target_branch: 'master'
- })
-
- within_wip_explanation do
- expect(page).not_to have_text(
- 'It looks like you have some draft commits in this branch'
- )
- expect(page).to have_text(
- "Start the title with Draft: to prevent a merge request draft \
-from merging before it's ready."
- )
- end
- end
- end
-
- def within_wip_explanation(&block)
- page.within '.js-no-wip-explanation' do
- yield
- end
- end
-end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index b7784de12b9..0de59ea21c5 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
@@ -3,13 +3,15 @@
require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
+ include ListboxHelpers
+
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
def select_source_branch(branch_name)
find('.js-source-branch', match: :first).click
- find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name
- find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click
+ find('.gl-listbox-search-input').native.send_keys branch_name
+ select_listbox_item(branch_name)
end
before do
@@ -27,7 +29,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content('Target branch')
first('.js-source-branch').click
- find('.js-source-branch-dropdown .dropdown-content a', match: :first).click
+ find('.gl-new-dropdown-item', match: :first).click
expect(page).to have_content "b83d6e3"
end
@@ -43,7 +45,8 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content('Target branch')
first('.js-target-branch').click
- find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click
+ find('.gl-listbox-search-input').native.send_keys 'v1.1.0'
+ select_listbox_item('v1.1.0')
expect(page).to have_content "b83d6e3"
end
@@ -68,27 +71,6 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\''
end
- it 'allows filtering multiple dropdowns' do
- visit project_new_merge_request_path(project)
-
- first('.js-source-branch').click
-
- page.within '.js-source-branch-dropdown' do
- input = find('.dropdown-input-field')
- input.click
- input.send_keys('orphaned-branch')
-
- expect(page).to have_css('.dropdown-content li', count: 1)
- end
-
- first('.js-target-branch').click
-
- find('.js-target-branch-dropdown .dropdown-content li', match: :first)
- target_items = all('.js-target-branch-dropdown .dropdown-content li')
-
- expect(target_items.count).to be > 1
- end
-
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 5a9054ece48..b0be76d386a 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -121,7 +121,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
within 'aside[aria-label="Bulk update"]' do
click_button 'Select assignee'
wait_for_requests
- click_link text
+ click_button text
end
click_update_merge_requests_button
end
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 cf99f2cb94a..58d796f8288 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -27,15 +27,15 @@ RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review_
visit(merge_requests_dashboard_path(assignee_username: user.username))
- expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
+ expect(find('.filter-dropdown-container button.gl-new-dropdown-toggle')).to have_content('Milestone')
visit(project_merge_requests_path(project))
- expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
+ expect(find('.filter-dropdown-container button.gl-new-dropdown-toggle')).to have_content('Milestone')
visit(merge_requests_group_path(group))
- expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
+ expect(find('.filter-dropdown-container button.gl-new-dropdown-toggle')).to have_content('Milestone')
end
it 'fallbacks to issuable_sort cookie key when remembering the sorting option' do
@@ -43,7 +43,7 @@ RSpec.describe 'User sorts merge requests', :js, feature_category: :code_review_
visit(merge_requests_dashboard_path(assignee_username: user.username))
- expect(find('.filter-dropdown-container button.dropdown-toggle')).to have_content('Milestone')
+ expect(find('.filter-dropdown-container button.gl-new-dropdown-toggle')).to have_content('Milestone')
end
it 'separates remember sorting with issues', :js do
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
index d038c5d9e32..56f9d373f00 100644
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -9,43 +9,87 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
before do
sign_in(user)
- visit explore_projects_path
resize_screen_xs
end
- context 'before opened' do
- it 'has page content and hides responsive menu', :aggregate_failures do
- expect(page).to have_css('.page-title', text: 'Projects')
- expect(page).to have_link('Dashboard', id: 'logo')
+ context 'when outside groups and projects' do
+ before do
+ visit explore_projects_path
+ end
- expect(page).to have_no_css('.top-nav-responsive')
+ context 'when menu is closed' do
+ it 'has page content and hides responsive menu', :aggregate_failures do
+ expect(page).to have_css('.page-title', text: 'Projects')
+ expect(page).to have_link('Dashboard', id: 'logo')
+
+ expect(page).to have_no_css('.top-nav-responsive')
+ end
+ end
+
+ context 'when menu is opened' do
+ before do
+ click_button('Menu')
+ end
+
+ it 'hides everything and shows responsive menu', :aggregate_failures do
+ expect(page).to have_no_css('.page-title', text: 'Projects')
+ expect(page).to have_no_link('Dashboard', id: 'logo')
+
+ within '.top-nav-responsive' do
+ expect(page).to have_link(nil, href: search_path)
+ expect(page).to have_button('Projects')
+ expect(page).to have_button('Groups')
+ expect(page).to have_link('Snippets', href: dashboard_snippets_path)
+ end
+ end
+
+ it 'has new dropdown', :aggregate_failures do
+ create_new_button.click
+
+ expect(page).to have_link('New project', href: new_project_path)
+ expect(page).to have_link('New group', href: new_group_path)
+ expect(page).to have_link('New snippet', href: new_snippet_path)
+ end
end
end
- context 'when opened' do
+ context 'when inside a project' do
+ let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
+
before do
- click_button('Menu')
+ visit project_path(project)
end
- it 'hides everything and shows responsive menu', :aggregate_failures do
- expect(page).to have_no_css('.page-title', text: 'Projects')
- expect(page).to have_no_link('Dashboard', id: 'logo')
+ it 'the add menu contains invite members dropdown option and goes to the members page' do
+ invite_members_from_menu
- within '.top-nav-responsive' do
- expect(page).to have_link(nil, href: search_path)
- expect(page).to have_button('Projects')
- expect(page).to have_button('Groups')
- expect(page).to have_link('Snippets', href: dashboard_snippets_path)
- end
+ expect(page).to have_current_path(project_project_members_path(project))
+ end
+ end
+
+ context 'when inside a group' do
+ let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
+
+ before do
+ visit group_path(group)
end
- it 'has new dropdown', :aggregate_failures do
- click_button('Create new...')
+ it 'the add menu contains invite members dropdown option and goes to the members page' do
+ invite_members_from_menu
- expect(page).to have_link('New project', href: new_project_path)
- expect(page).to have_link('New group', href: new_group_path)
- expect(page).to have_link('New snippet', href: new_snippet_path)
+ expect(page).to have_current_path(group_group_members_path(group))
end
end
+
+ def invite_members_from_menu
+ click_button('Menu')
+ create_new_button.click
+
+ click_link('Invite members')
+ end
+
+ def create_new_button
+ find('[data-testid="plus-icon"]')
+ end
end
diff --git a/spec/features/nav/top_nav_spec.rb b/spec/features/nav/top_nav_spec.rb
new file mode 100644
index 00000000000..cc20b626e30
--- /dev/null
+++ b/spec/features/nav/top_nav_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when inside a project' do
+ let_it_be(:project) { create(:project).tap { |record| record.add_owner(user) } }
+
+ before do
+ visit project_path(project)
+ end
+
+ it 'the add menu contains invite members dropdown option and goes to the members page' do
+ invite_members_from_menu
+
+ expect(page).to have_current_path(project_project_members_path(project))
+ end
+ end
+
+ context 'when inside a group' do
+ let_it_be(:group) { create(:group).tap { |record| record.add_owner(user) } }
+
+ before do
+ visit group_path(group)
+ end
+
+ it 'the add menu contains invite members dropdown option and goes to the members page' do
+ invite_members_from_menu
+
+ expect(page).to have_current_path(group_group_members_path(group))
+ end
+ end
+
+ def invite_members_from_menu
+ find('[data-testid="new-dropdown"]').click
+
+ click_link('Invite members')
+ end
+end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index 07d0fca0139..bd96d65f984 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'OAuth Login', :allow_forgery_protection, feature_category: :syst
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
- :facebook, :cas3, :auth0, :authentiq, :salesforce, :dingtalk, :alicloud]
+ :facebook, :cas3, :auth0, :salesforce, :dingtalk, :alicloud]
around do |example|
with_omniauth_full_host { example.run }
diff --git a/spec/features/oauth_registration_spec.rb b/spec/features/oauth_registration_spec.rb
index 6e1445a9ed6..3c1004e452f 100644
--- a/spec/features/oauth_registration_spec.rb
+++ b/spec/features/oauth_registration_spec.rb
@@ -23,7 +23,6 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection, feature_cat
:facebook | {}
:cas3 | {}
:auth0 | {}
- :authentiq | {}
:salesforce | { extra: { email_verified: true } }
:dingtalk | {}
:alicloud | {}
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 87a65438768..e190dfda937 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, feature_category: :users do
+RSpec.describe 'Profile account page', :js, feature_category: :user_profile 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 82c45862e07..7e4308106be 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, feature_category: :users do
+RSpec.describe 'Profile > Account', :js, feature_category: :user_profile 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 5c20735cf35..0de4ad47f9a 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, feature_category: :users do
+RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, feature_category: :user_profile 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 14fdb8ba56f..299ecdb6032 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', feature_category: :users do
+RSpec.describe 'Profile > Chat', feature_category: :user_profile 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 e8ea227c072..d00cef1f6f0 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', feature_category: :users do
+RSpec.describe 'Profile > Emails', feature_category: :user_profile 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 1d014f983e7..0fc59f21489 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', feature_category: :users do
+RSpec.describe 'Profile > GPG Keys', feature_category: :user_profile 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 7a2a12d8dca..ae61f1cf492 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', feature_category: :users do
+RSpec.describe 'Profile > SSH Keys', feature_category: :user_profile do
let(:user) { create(:user) }
before do
@@ -76,35 +76,76 @@ RSpec.describe 'Profile > SSH Keys', feature_category: :users do
expect(page).to have_content(key.title)
end
+ def destroy_key(path, action, confirmation_button)
+ visit path
+
+ page.click_button(action)
+
+ page.within('.modal') do
+ page.click_button(confirmation_button)
+ end
+
+ expect(page).to have_content('Your SSH keys (0)')
+ end
+
describe 'User removes a key', :js do
- shared_examples 'removes key' do
- it 'removes key' do
- visit path
- find('[data-testid=remove-icon]').click
+ let!(:key) { create(:key, user: user) }
- page.within('.modal') do
- page.click_button('Delete')
- end
+ context 'via the key index' do
+ it 'removes key' do
+ destroy_key(profile_keys_path, 'Remove', 'Delete')
+ end
+ end
- expect(page).to have_content('Your SSH keys (0)')
+ context 'via its details page' do
+ it 'removes key' do
+ destroy_key(profile_keys_path(key), 'Remove', 'Delete')
end
end
+ end
+
+ describe 'User revokes a key', :js do
+ context 'when a commit is signed using SSH key' do
+ let!(:project) { create(:project, :repository) }
+ let!(:key) { create(:key, user: user) }
+ let!(:commit) { project.commit('ssh-signed-commit') }
+
+ let!(:signature) do
+ create(:ssh_signature,
+ project: project,
+ key: key,
+ key_fingerprint_sha256: key.fingerprint_sha256,
+ commit_sha: commit.sha)
+ end
- context 'via the key index' do
before do
- create(:key, user: user)
+ project.add_developer(user)
end
- let(:path) { profile_keys_path }
+ it 'revoking the SSH key marks commits as unverified' do
+ visit project_commit_path(project, commit)
+ wait_for_all_requests
- it_behaves_like 'removes key'
- end
+ find('a.signature-badge', text: 'Verified').click
- context 'via its details page' do
- let(:key) { create(:key, user: user) }
- let(:path) { profile_keys_path(key) }
+ within('.popover') do
+ expect(page).to have_content("Verified commit")
+ expect(page).to have_content("SSH key fingerprint: #{key.fingerprint_sha256}")
+ end
+
+ destroy_key(profile_keys_path, 'Revoke', 'Revoke')
+
+ visit project_commit_path(project, commit)
+ wait_for_all_requests
- it_behaves_like 'removes key'
+ find('a.signature-badge', text: 'Unverified').click
+
+ within('.popover') do
+ expect(page).to have_content("Unverified signature")
+ expect(page).to have_content('This commit was signed with a key that was revoked.')
+ expect(page).to have_content("SSH key fingerprint: #{signature.key_fingerprint_sha256}")
+ end
+ end
end
end
end
diff --git a/spec/features/profiles/list_users_saved_replies_spec.rb b/spec/features/profiles/list_users_saved_replies_spec.rb
new file mode 100644
index 00000000000..4f3678f8051
--- /dev/null
+++ b/spec/features/profiles/list_users_saved_replies_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Profile > Notifications > List users saved replies', :js,
+ feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the user a list of their saved replies' do
+ visit profile_saved_replies_path
+
+ expect(page).to have_content('My saved replies (1)')
+ expect(page).to have_content(saved_reply.name)
+ expect(page).to have_content(saved_reply.content)
+ end
+end
diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb
index 80d05fd5cc7..d088f73f9df 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', feature_category: :users do
+RSpec.describe 'Profile > Applications', feature_category: :user_profile 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 b324ee17873..c0c573d2f20 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', feature_category: :users do
+RSpec.describe 'Profile > Password', feature_category: :user_profile 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 3087d7ff296..a050e87241b 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, feature_category: :users do
+RSpec.describe 'Profile > Personal Access Tokens', :js, feature_category: :user_profile 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 8dddaad11c3..e8ff8416722 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', feature_category: :users do
+RSpec.describe 'Two factor auths', feature_category: :user_profile 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 197a33c355d..89887cb4772 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
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Profile > Notifications > User changes notified_of_own_activity setting', :js,
-feature_category: :users do
+feature_category: :user_profile 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 1a231f1d269..f7a9850355a 100644
--- a/spec/features/profiles/user_edit_preferences_spec.rb
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User edit preferences profile', :js, feature_category: :users do
+RSpec.describe 'User edit preferences profile', :js, feature_category: :user_profile do
include StubLanguagesTranslationPercentage
# Empty value doesn't change the levels
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 67604292090..3819723cc09 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', feature_category: :users do
+RSpec.describe 'User edit profile', feature_category: :user_profile 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 179da61b8ed..e3c4a797431 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', feature_category: :users do
+RSpec.describe 'User manages applications', feature_category: :user_profile 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 16a9fbc2f47..b875dfec217 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', feature_category: :users do
+RSpec.describe 'User manages emails', feature_category: :user_profile 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 09ee8ddeaab..932ea11075a 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, feature_category: :users do
+RSpec.describe 'User searches their settings', :js, feature_category: :user_profile 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 d212982f4e3..1295a0b6150 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, feature_category: :users do
+RSpec.describe 'User visits the notifications tab', :js, feature_category: :user_profile 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 1cf34478ecf..8ff9cbc242e 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', feature_category: :users do
+RSpec.describe 'User visits the profile account page', feature_category: :user_profile 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 726cca4a4bd..90f24c5b866 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', feature_category: :users do
+RSpec.describe 'User visits the authentication log', feature_category: :user_profile 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 e3940973c46..d690589b893 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, feature_category: :users do
+RSpec.describe 'User visits the profile preferences page', :js, feature_category: :user_profile do
include ListboxHelpers
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 7fca0f24deb..ad265fbae9e 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -2,10 +2,11 @@
require 'spec_helper'
-RSpec.describe 'User visits their profile', feature_category: :users do
+RSpec.describe 'User visits their profile', feature_category: :user_profile do
let_it_be_with_refind(:user) { create(:user) }
before do
+ stub_feature_flags(profile_tabs_vue: false)
sign_in(user)
end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
index 8467e9abeaf..547e47ead77 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', feature_category: :users do
+RSpec.describe 'User visits the profile SSH keys page', feature_category: :user_profile do
let(:user) { create(:user) }
before do
diff --git a/spec/features/projects/artifacts/user_views_project_artifacts_page_spec.rb b/spec/features/projects/artifacts/user_views_project_artifacts_page_spec.rb
new file mode 100644
index 00000000000..f1601348e57
--- /dev/null
+++ b/spec/features/projects/artifacts/user_views_project_artifacts_page_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe 'User views project artifacts page', :js, feature_category: :build_artifacts do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let_it_be(:job_with_artifacts) { create(:ci_build, :artifacts, name: 'test1', pipeline: pipeline) }
+ let_it_be(:job_with_trace) { create(:ci_build, :trace_artifact, name: 'test3', pipeline: pipeline) }
+ let_it_be(:job_without_artifacts) { create(:ci_build, name: 'test2', pipeline: pipeline) }
+
+ let(:path) { project_artifacts_path(project) }
+
+ context 'when browsing artifacts page' do
+ before do
+ visit(path)
+
+ wait_for_requests
+ end
+
+ it 'lists the project jobs and their artifacts' do
+ page.within('main#content-body') do
+ page.within('table thead') do
+ expect(page).to have_content('Artifacts')
+ .and have_content('Job')
+ .and have_content('Size')
+ end
+
+ find_all('[data-testid="job-artifacts-count"').each(&:click)
+
+ expect(page).to have_content(job_with_artifacts.name)
+ expect(page).to have_content(job_with_trace.name)
+ expect(page).not_to have_content(job_without_artifacts.name)
+
+ expect(page).to have_content('archive').and have_content('metadata')
+ expect(page).to have_content('trace')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index b7e0e3fd590..7faf0e1a6b1 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -961,8 +961,8 @@ RSpec.describe 'File blob', :js, feature_category: :projects do
end
it 'renders sandboxed iframe' do
- expected = %(<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000">)
- expect(page.html).to include(expected)
+ expected = %(iframe[src$="/-/sandbox/swagger"][sandbox="allow-scripts allow-popups allow-forms"][frameborder="0"][width="100%"][height="1000"])
+ expect(page).to have_css(expected)
end
end
end
@@ -1007,8 +1007,8 @@ RSpec.describe 'File blob', :js, feature_category: :projects do
it 'displays a GPG badge' do
visit_blob('CONTRIBUTING.md', ref: '33f3729a45c02fc67d00adb1b8bca394b0e761d9')
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
@@ -1016,8 +1016,8 @@ RSpec.describe 'File blob', :js, feature_category: :projects do
it 'displays a GPG badge' do
visit_blob('conflicting-file.md', ref: '6101e87e575de14b38b4e1ce180519a813671e10')
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
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 eb370cfc1fc..9afd8b3263a 100644
--- a/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
+++ b/spec/features/projects/branches/new_branch_ref_dropdown_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:sha) { project.commit.sha }
@@ -18,15 +20,13 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a tag in a list' do
tag_name = 'v1.0.0'
- toggle.click
-
filter_by(tag_name)
wait_for_requests
expect(items_count(tag_name)).to be(1)
- item(tag_name).click
+ select_listbox_item tag_name
expect(toggle).to have_content tag_name
end
@@ -34,22 +34,18 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
it 'finds a branch in a list' do
branch_name = 'audio'
- toggle.click
-
filter_by(branch_name)
wait_for_requests
expect(items_count(branch_name)).to be(1)
- item(branch_name).click
+ select_listbox_item branch_name
expect(toggle).to have_content branch_name
end
it 'finds a commit in a list' do
- toggle.click
-
filter_by(sha)
wait_for_requests
@@ -58,21 +54,19 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
expect(items_count(sha_short)).to be(1)
- item(sha_short).click
+ select_listbox_item sha_short
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)
+ click_button 'master'
+ expect(toggle).not_to have_content(non_existing_ref)
end
def item(ref_name)
@@ -84,6 +78,7 @@ RSpec.describe 'New Branch Ref Dropdown', :js, feature_category: :projects do
end
def filter_by(filter_text)
- fill_in _('Search by Git revision'), with: filter_text
+ click_button 'master'
+ send_keys filter_text
end
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index dc8b84283a1..93ce851521f 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -77,10 +77,12 @@ RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_manage
click_button 'master'
end
- page.within("#{modal_selector} .dropdown-menu") do
- find('[data-testid="dropdown-search-box"]').set('feature')
+ page.within("#{modal_selector} [data-testid=\"base-dropdown-menu\"]") do
+ fill_in 'Search branches', with: 'feature'
+
wait_for_requests
- click_button 'feature'
+
+ find('[data-testid="listbox-item-feature"]').click
end
submit_cherry_pick
diff --git a/spec/features/projects/commits/multi_view_diff_spec.rb b/spec/features/projects/commits/multi_view_diff_spec.rb
index b178a1c2171..f0a074e9b7f 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", feature_category: :source_code_management do |commit_ref|
+RSpec.shared_examples "no multiple viewers" do |commit_ref|
let(:ref) { commit_ref }
it "does not display multiple diff viewers" do
@@ -10,7 +10,7 @@ RSpec.shared_examples "no multiple viewers", feature_category: :source_code_mana
end
end
-RSpec.describe 'Multiple view Diffs', :js do
+RSpec.describe 'Multiple view Diffs', :js, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 125f7209ab4..8082d1bdf63 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -4,6 +4,7 @@ require "spec_helper"
RSpec.describe "User browses files", :js, feature_category: :projects do
include RepoHelpers
+ include ListboxHelpers
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
@@ -282,17 +283,13 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
expect(page).to have_content(".gitignore").and have_content("LICENSE")
end
- it "shows files from a repository with apostroph in its name" do
- ref_name = 'test'
+ it "shows files from a repository with apostrophe in its name" do
+ ref_name = 'fix'
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
+ filter_by(ref_name)
expect(find(ref_selector)).to have_text(ref_name)
@@ -307,11 +304,7 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
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
+ filter_by(ref_name)
visit(project_tree_path(project, "fix/.testdir"))
@@ -345,8 +338,8 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
.and have_content("Initial commit")
.and have_content("Ignore DS files")
- previous_commit_anchor = "//a[@title='Ignore DS files']/parent::span/following-sibling::span/a"
- find(:xpath, previous_commit_anchor).click
+ previous_commit_link = find('.tr', text: "Ignore DS files").find("[aria-label='View blame prior to this change']")
+ previous_commit_link.click
expect(page).to have_content("*.rb")
.and have_content("Dmitriy Zaporozhets")
@@ -394,4 +387,12 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
end
end
end
+
+ def filter_by(filter_text)
+ send_keys filter_text
+
+ wait_for_requests
+
+ select_listbox_item filter_text
+ end
end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
index 1b53189da83..9cc2ce6a7b4 100644
--- a/spec/features/projects/files/user_find_file_spec.rb
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User find project file', feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create :user }
let(:project) { create :project, :repository }
@@ -21,6 +23,10 @@ RSpec.describe 'User find project file', feature_category: :projects do
fill_in 'file_find', with: text
end
+ def ref_selector_dropdown
+ find('.gl-button-text')
+ end
+
it 'navigates to find file by shortcut', :js do
find('body').native.send_key('t')
@@ -65,4 +71,39 @@ RSpec.describe 'User find project file', feature_category: :projects do
expect(page).not_to have_content('CHANGELOG')
expect(page).not_to have_content('VERSION')
end
+
+ context 'when refs are switched', :js do
+ before do
+ click_link 'Find file'
+ end
+
+ specify 'the ref switcher lists all the branches and tags' do
+ ref = 'add-ipython-files'
+ expect(ref_selector_dropdown).not_to have_text(ref)
+
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: ref)
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
+ end
+
+ specify 'the search result changes when refs switched' do
+ ref = 'add-ipython-files'
+ expect(ref_selector_dropdown).not_to have_text(ref)
+
+ find('.ref-selector button').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in _('Switch branch/tag'), with: ref
+ wait_for_requests
+
+ select_listbox_item(ref)
+ end
+ expect(ref_selector_dropdown).to have_text(ref)
+ end
+ end
end
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index f96356b11c9..a1f047d9b43 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -59,23 +59,30 @@ RSpec.describe 'Project Graph', :js, feature_category: :projects do
it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
- expect(page.find('.dropdown-toggle-text')['innerHTML']).to eq(ERB::Util.html_escape(branch_name))
+ expect(page.find('.gl-new-dropdown-button-text')['innerHTML']).to include(ERB::Util.html_escape(branch_name))
end
end
context 'charts graph ref switcher' do
it 'switches ref to branch' do
- ref_name = 'feature'
+ ref_name = 'add-pdf-file'
visit charts_project_graph_path(project, 'master')
- first('.js-project-refs-dropdown').click
- page.within '.project-refs-form' do
- click_link ref_name
+ # Not a huge fan of using a HTML (CSS) selectors here as any change of them will cause a failed test
+ ref_selector = find('.ref-selector .gl-new-dropdown-toggle')
+ scroll_to(ref_selector)
+ ref_selector.click
+
+ page.within '.gl-new-dropdown-contents' do
+ dropdown_branch_item = find('li', text: 'add-pdf-file')
+ scroll_to(dropdown_branch_item)
+ dropdown_branch_item.click
end
- expect(page).to have_selector '.dropdown-menu-toggle', text: ref_name
+ scroll_to(find('.tree-ref-header'), align: :center)
+ expect(page).to have_selector '.gl-new-dropdown-toggle', text: ref_name
page.within '.tree-ref-header' do
- expect(page).to have_content ref_name
+ expect(page).to have_selector('h4', text: ref_name)
end
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 0230c9e835b..6630956f835 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -7,7 +7,6 @@ require 'spec_helper'
# 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, feature_category: :importers do
- include Select2Helper
include ExportFileHelper
let(:user) { create(:admin) }
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 01c202baf70..ec00dcaf046 100644
--- a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'User activates Slack notifications', :js, feature_category: :int
context 'when integration is not configured yet' do
before do
+ stub_feature_flags(integration_slack_app_notifications: false)
visit_project_integration('Slack notifications')
end
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
index 11c8bdda3ac..268c209cba1 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
@@ -24,4 +24,26 @@ RSpec.describe 'User views issue designs', :js, feature_category: :design_manage
expect(page).to have_selector('.js-design-image')
end
+
+ context 'when svg file is loaded in design detail' do
+ let_it_be(:file) { Rails.root.join('spec/fixtures/svg_without_attr.svg') }
+ let_it_be(:design) { create(:design, :with_file, filename: 'svg_without_attr.svg', file: file, issue: issue) }
+
+ before do
+ visit designs_project_issue_path(
+ project,
+ issue,
+ { vueroute: design.filename }
+ )
+ wait_for_requests
+ end
+
+ it 'check if svg is loading' do
+ expect(page).to have_selector(
+ ".js-design-image > img[alt='svg_without_attr.svg']",
+ count: 1,
+ visible: :hidden
+ )
+ end
+ end
end
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 a45b9b718c3..bbc54382ae6 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
@@ -42,7 +42,7 @@ RSpec.describe 'User views an SVG design that contains XSS', :js, feature_catego
}
# With the page loaded, there should be no alert modal
- expect(run_expectation).to raise_error(
+ expect { run_expectation.call }.to raise_error(
Capybara::ModalNotFound,
'Unable to find modal dialog'
)
@@ -51,6 +51,6 @@ RSpec.describe 'User views an SVG design that contains XSS', :js, feature_catego
# With an alert modal displaying, the modal should be dismissable.
execute_script('alert(true)')
- expect(run_expectation).not_to raise_error
+ expect { run_expectation.call }.not_to raise_error
end
end
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
index 4dedbff608e..a902c8294d7 100644
--- a/spec/features/projects/issues/email_participants_spec.rb
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe 'viewing an issue', :js, feature_category: :issue_email_participants do
+RSpec.describe 'viewing an issue', :js, feature_category: :service_desk do
let_it_be(:user) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be_with_refind(:issue) { create(:issue, project: project) }
let_it_be(:note) { create(:note_on_issue, project: project, noteable: issue) }
@@ -19,9 +20,17 @@ RSpec.describe 'viewing an issue', :js, feature_category: :issue_email_participa
end
end
- 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")
+ shared_examples 'email participants warning in all editors' do
+ 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
@@ -32,35 +41,42 @@ RSpec.describe 'viewing an issue', :js, feature_category: :issue_email_participa
visit project_issue_path(project, issue)
end
- context 'for a new note' do
- it_behaves_like 'email participants warning', '.new-note'
- end
+ it_behaves_like 'email participants warning in all editors'
+ end
- context 'for a reply form' do
- before do
- find('.js-reply-button').click
+ context 'when issue is not confidential' do
+ context 'with signed in user' do
+ context 'when user has no role in project' do
+ before do
+ sign_in(non_member)
+ visit project_issue_path(project, issue)
+ end
+
+ it_behaves_like 'email participants warning in all editors'
end
- it_behaves_like 'email participants warning', '.note-edit-form'
+ context 'when user has (at least) reporter role in project' do
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it_behaves_like 'email participants warning in all editors'
+ end
end
end
- context 'when issue is not confidential' do
+ context 'for feature flags' do
before do
sign_in(user)
- visit project_issue_path(project, issue)
end
- context 'for a new note' do
- it_behaves_like 'no email participants warning', '.new-note'
- end
+ it 'pushes service_desk_new_note_email_native_attachments feature flag to frontend' do
+ stub_feature_flags(service_desk_new_note_email_native_attachments: true)
- context 'for a reply form' do
- before do
- find('.js-reply-button').click
- end
+ visit project_issue_path(project, issue)
- it_behaves_like 'no email participants warning', '.note-edit-form'
+ expect(page).to have_pushed_frontend_feature_flags(serviceDeskNewNoteEmailNativeAttachments: true)
end
end
end
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 78fb72ad2df..dd57b4117f9 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -34,10 +34,6 @@ RSpec.describe 'User browses a job', :js, feature_category: :projects do
wait_for_requests
expect(page).to have_no_css('.artifacts')
- expect(build).not_to have_trace
- expect(build.artifacts_file.present?).to be_falsy
- expect(build.artifacts_metadata.present?).to be_falsy
-
expect(page).to have_content('Job has been erased')
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 4734a607ef1..67389fdda8a 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -302,7 +302,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :proj
click_link 'Download'
end
- artifact_request = requests.find { |req| req.url.match(%r{artifacts/download}) }
+ artifact_request = requests.find { |req| req.url.include?('artifacts/download') }
expect(artifact_request.response_headers['Content-Disposition']).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"; filename*=UTF-8''#{job.artifacts_file.filename}})
expect(artifact_request.response_headers['Content-Transfer-Encoding']).to eq("binary")
@@ -745,11 +745,11 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :proj
'You can add CI/CD variables below for last-minute configuration changes before starting the job.'
)
)
- expect(page).to have_button('Trigger this manual action')
+ expect(page).to have_button('Run job')
end
it 'plays manual action and shows pending status', :js do
- click_button 'Trigger this manual action'
+ click_button 'Run job'
wait_for_requests
expect(page).to have_content('This job has not started yet')
@@ -783,7 +783,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :proj
'You can add CI/CD variables below for last-minute configuration changes before starting the job.'
)
)
- expect(page).to have_button('Trigger this manual action')
+ expect(page).to have_button('Run job')
end
end
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index b36fde8a2bf..a29c9f58195 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
let(:user) { create :user }
let(:project) { create :project, :repository, namespace: user.namespace }
+ let(:ref_selector) { '.ref-selector' }
before do
sign_in(user)
@@ -16,10 +17,13 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
shared_examples 'network graph' do
context 'when branch is master' do
def switch_ref_to(ref_name)
- first('.js-project-refs-dropdown').click
+ first(ref_selector).click
+ wait_for_requests
- page.within '.project-refs-form' do
- click_link ref_name
+ 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
end
@@ -33,7 +37,7 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
it 'renders project network' do
expect(page).to have_selector ".network-graph"
- expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
+ expect(page).to have_selector ref_selector, text: "master"
page.within '.network-graph' do
expect(page).to have_content 'master'
end
@@ -42,7 +46,7 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
it 'switches ref to branch' do
switch_ref_to('feature')
- expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature'
+ expect(page).to have_selector ref_selector, text: 'feature'
page.within '.network-graph' do
expect(page).to have_content 'feature'
end
@@ -51,7 +55,7 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
it 'switches ref to tag' do
switch_ref_to('v1.0.0')
- expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0'
+ expect(page).to have_selector ref_selector, text: 'v1.0.0'
page.within '.network-graph' do
expect(page).to have_content 'v1.0.0'
end
@@ -64,7 +68,7 @@ RSpec.describe 'Project Network Graph', :js, feature_category: :projects do
end
expect(page).to have_selector ".network-graph"
- expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
+ expect(page).to have_selector ref_selector, text: "master"
page.within '.network-graph' do
expect(page).to have_content 'v1.0.0'
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 8beb8af1a8e..3ede76d3360 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-button-text').text).to eq('master')
end
end
end
@@ -77,7 +77,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
it 'shows the pipeline schedule with default ref' do
page.within('[data-testid="schedule-target-ref"]') do
- expect(first('.gl-dropdown-button-text').text).to eq('master')
+ expect(first('.gl-button-text').text).to eq('master')
end
end
end
@@ -319,7 +319,6 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :projects do
end
def select_target_branch
- find('[data-testid="schedule-target-ref"] .dropdown-toggle').click
click_button 'master'
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 e69de29bb2d..00000000000
--- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb
+++ /dev/null
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index d5739386a30..343c7f53022 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1233,12 +1233,27 @@ RSpec.describe 'Pipeline', :js, feature_category: :projects do
it 'displays the pipeline graph' do
subject
- expect(page).to have_current_path(pipeline_path(pipeline), ignore_query: true)
+ expect(page).to have_current_path(pipeline_path(pipeline))
expect(page).to have_selector('.js-pipeline-graph')
end
end
end
+ describe 'GET /:project/-/pipelines/latest' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ visit latest_project_pipelines_path(project)
+ end
+
+ it 'displays the pipeline graph with correct URL' do
+ expect(page).to have_current_path("#{pipeline_path(pipeline)}/")
+ expect(page).to have_selector('.js-pipeline-graph')
+ end
+ end
+
describe 'GET /:project/-/pipelines/:id/dag' do
include_context 'pipeline builds'
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 6a44f421249..b5f640f1cca 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -597,8 +597,8 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
it 'changes the Pipeline ID column for Pipeline IID' do
page.find('[data-testid="pipeline-key-collapsible-box"]').click
- within '.gl-dropdown-contents' do
- dropdown_options = page.find_all '.gl-dropdown-item'
+ within '.gl-new-dropdown-contents' do
+ dropdown_options = page.find_all '.gl-new-dropdown-item'
dropdown_options[1].click
end
@@ -675,7 +675,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
click_button project.default_branch
wait_for_requests
- find('.gl-dropdown-item', text: 'master').click
+ find('.gl-new-dropdown-item', text: 'master').click
wait_for_requests
end
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 ef3b35837ff..b3f21a2d328 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
end
it 'renders the edit Release form' do
- expect(page).to have_content('Releases are based on Git tags. We recommend tags that use semantic versioning, for example v1.0.0, v2.1.0-pre.')
+ expect(page).to have_content('Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.')
expect(find_field('Tag name', disabled: true).value).to eq(release.tag)
expect(find_field('Release title').value).to eq(release.name)
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index 13dde57d885..0a4075be02f 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -46,10 +46,10 @@ RSpec.describe 'User views releases', :js, feature_category: :continuous_deliver
external_link_indicator_selector = '[data-testid="external-link-indicator"]'
expect(page).to have_link internal_link.name, href: internal_link.url
- expect(find_link(internal_link.name)).not_to have_css(external_link_indicator_selector)
+ expect(find_link(internal_link.name)).to have_css(external_link_indicator_selector)
expect(page).to have_link internal_link_with_redirect.name, href: Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{internal_link_with_redirect.filepath}"
- expect(find_link(internal_link_with_redirect.name)).not_to have_css(external_link_indicator_selector)
+ expect(find_link(internal_link_with_redirect.name)).to have_css(external_link_indicator_selector)
expect(page).to have_link external_link.name, href: external_link.url
expect(find_link(external_link.name)).to have_css(external_link_indicator_selector)
diff --git a/spec/features/projects/settings/packages_settings_spec.rb b/spec/features/projects/settings/packages_settings_spec.rb
index 4ef17830f81..bf5c779b109 100644
--- a/spec/features/projects/settings/packages_settings_spec.rb
+++ b/spec/features/projects/settings/packages_settings_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe 'Projects > Settings > Packages', :js, feature_category: :project
sign_in(user)
stub_config(packages: { enabled: packages_enabled })
- stub_feature_flags(package_registry_access_level: package_registry_access_level)
visit edit_project_path(project)
end
@@ -19,35 +18,21 @@ RSpec.describe 'Projects > Settings > Packages', :js, feature_category: :project
context 'Packages enabled in config' do
let(:packages_enabled) { true }
- context 'with feature flag disabled' do
- let(:package_registry_access_level) { false }
-
- it 'displays the packages toggle button' do
- expect(page).to have_selector('[data-testid="toggle-label"]', text: 'Packages')
- expect(page).to have_selector('input[name="project[packages_enabled]"] + button', visible: true)
- end
- end
-
- context 'with feature flag enabled' do
- let(:package_registry_access_level) { true }
-
- 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
- )
- end
+ 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
+ )
end
end
context 'Packages disabled in config' do
let(:packages_enabled) { false }
- let(:package_registry_access_level) { false }
it 'does not show up in UI' do
expect(page).not_to have_selector('[data-testid="toggle-label"]', text: 'Packages')
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 6f0a3094849..a0625c93b1a 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :p
before do
stub_feature_flags(branch_rules: false)
+ stub_feature_flags(mirror_only_branches_match_regex: false)
project.add_role(user, role)
sign_in(user)
end
diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb
index 39704fdbbb2..67ba16a2716 100644
--- a/spec/features/projects/settings/user_changes_default_branch_spec.rb
+++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Projects > Settings > User changes default branch', feature_category: :projects do
+ include ListboxHelpers
+
let(:user) { create(:user) }
before do
@@ -20,10 +22,10 @@ RSpec.describe 'Projects > Settings > User changes default branch', feature_cate
wait_for_requests
expect(page).to have_selector(dropdown_selector)
- find(dropdown_selector).click
+ click_button 'master'
+ send_keys 'fix'
- fill_in 'Search branch', with: 'fix'
- click_button 'fix'
+ select_listbox_item 'fix'
page.within '#branch-defaults-settings' do
click_button 'Save changes'
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 fac4d5a99a5..159a83a261d 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Settings > User manages project members', feature_category: :projects do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::ModalHelpers
+ include ListboxHelpers
let(:group) { create(:group, name: 'OpenSource') }
let(:project) { create(:project, :with_namespace_settings) }
@@ -46,7 +47,7 @@ RSpec.describe 'Projects > Settings > User manages project members', feature_cat
click_on 'Select a project'
wait_for_requests
- click_button project2.name
+ select_listbox_item(project2.name_with_namespace)
click_button 'Import project members'
wait_for_requests
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index 8d22d84b9c9..3b8b982b621 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -82,8 +82,8 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :proj
WebMock.stub_request(:post, hook.url)
visit webhooks_path
- find('.hook-test-button.dropdown').click
- click_link 'Push events'
+ click_button 'Test'
+ click_button 'Push events'
expect(page).to have_current_path(webhooks_path, ignore_query: true)
end
diff --git a/spec/features/projects/show/clone_button_spec.rb b/spec/features/projects/show/clone_button_spec.rb
new file mode 100644
index 00000000000..48af4bf8277
--- /dev/null
+++ b/spec/features/projects/show/clone_button_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects > Show > Clone button', feature_category: :projects do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :in_group, :repository) }
+
+ describe 'when checking project main page user' do
+ context 'with an admin role' do
+ before do
+ project.add_owner(admin)
+ sign_in(admin)
+ visit project_path(project)
+ end
+
+ it 'is able to access project page' do
+ expect(page).to have_content project.name
+ end
+
+ it 'sees clone button' do
+ expect(page).to have_content _('Clone')
+ end
+ end
+
+ context 'with a guest role and no download_code access' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_path(project)
+ end
+
+ it 'is able to access project page' do
+ expect(page).to have_content project.name
+ end
+
+ it 'does not see clone button' do
+ expect(page).not_to have_content _('Clone')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index f2c575231ad..06e48bc82c0 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, feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > Create Snippet', :js, feature_category: :source_code_management 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 1a480696b4e..12018b4b9d7 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, feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > Project snippet', :js, feature_category: :source_code_management 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 556f549f86c..a153298da8e 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, feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > User comments on a snippet', :js, feature_category: :source_code_management 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 c9d1afb7a4e..6a825fe18de 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, feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > User deletes a snippet', :js, feature_category: :source_code_management 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 205db6c08b1..014bf63c696 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, feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > User updates a snippet', :js, feature_category: :source_code_management 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 ece65763ea5..a6d1db2b02f 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', feature_category: :snippets do
+RSpec.describe 'Projects > Snippets > User views snippets', feature_category: :source_code_management do
let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 835a3cda65e..52c6cb2192b 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
include WebIdeSpecHelpers
include RepoHelpers
+ include ListboxHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -93,8 +94,8 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
visit project_tree_path(project, '33f3729a45c02fc67d00adb1b8bca394b0e761d9')
wait_for_requests
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
context 'on a directory that has not changed recently' do
@@ -103,8 +104,8 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
visit project_tree_path(project, tree_path)
wait_for_requests
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
end
@@ -151,26 +152,22 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
visit project_tree_path(project, '33f3729a45c02fc67d00adb1b8bca394b0e761d9')
wait_for_requests
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
end
context 'ref switcher', :js do
- it 'switches ref to branch' do
+ it 'switches ref to branch', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391780' do
ref_selector = '.ref-selector'
- ref_name = 'feature'
+ ref_name = 'fix'
visit project_tree_path(project, 'master')
- find(ref_selector).click
- wait_for_requests
+ click_button 'master'
+ send_keys ref_name
- 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
+ select_listbox_item ref_name
expect(find(ref_selector)).to have_text(ref_name)
end
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
index 352fa73bd05..e2b56e8ced6 100644
--- a/spec/features/projects/user_views_empty_project_spec.rb
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'User views an empty project', feature_category: :projects do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) }
@@ -29,7 +31,9 @@ RSpec.describe 'User views an empty project', feature_category: :projects do
click_button 'Invite members'
- expect(page).to have_content("You're inviting members to the")
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 84702b3a6bb..73ee250a8b8 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -343,8 +343,8 @@ RSpec.describe 'Project', feature_category: :projects do
visit project_path(project)
wait_for_requests
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
@@ -371,8 +371,8 @@ RSpec.describe 'Project', feature_category: :projects do
visit project_path(project)
wait_for_requests
- expect(page).not_to have_selector '.gpg-status-box.js-loading-gpg-badge'
- expect(page).to have_selector '.gpg-status-box.invalid'
+ expect(page).not_to have_selector '.gl-badge.js-loading-signature-badge'
+ expect(page).to have_selector '.gl-badge.badge-muted'
end
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index e7c2452af93..b2ddf427c0d 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -64,10 +64,10 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
context 'when a project_type runner is activated on the project' do
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
- it 'user sees the specific runner' do
+ it 'user sees the project runner' do
visit project_runners_path(project)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
expect(page).to have_content(project_runner.display_name)
end
@@ -76,30 +76,30 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
expect(page).to have_content(project_runner.platform)
end
- it 'user can pause and resume the specific runner' do
+ it 'user can pause and resume the project runner' do
visit project_runners_path(project)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
expect(page).to have_link('Pause')
end
click_on 'Pause'
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
expect(page).to have_link('Resume')
end
click_on 'Resume'
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
expect(page).to have_link('Pause')
end
end
- it 'user removes an activated specific runner if this is last project for that runners' do
+ it 'user removes an activated project runner if this is last project for that runners' do
visit project_runners_path(project)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
click_on 'Remove runner'
end
@@ -109,7 +109,7 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
it 'user edits the runner to be protected' do
visit project_runners_path(project)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
first('[data-testid="edit-runner-link"]').click
end
@@ -129,7 +129,7 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
it 'user edits runner not to run untagged jobs' do
visit project_runners_path(project)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
first('[data-testid="edit-runner-link"]').click
end
@@ -189,7 +189,7 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
end
end
- context 'when a specific runner exists in another project' do
+ context 'when a project runner exists in another project' do
let(:another_project) { create(:project) }
let!(:project_runner) { create(:ci_runner, :project, projects: [another_project]) }
@@ -197,20 +197,20 @@ RSpec.describe 'Runners', feature_category: :runner_fleet do
another_project.add_maintainer(user)
end
- it 'user enables and disables a specific runner' do
+ it 'user enables and disables a project runner' do
visit project_runners_path(project)
- within '.available-specific-runners' do
+ within '[data-testid="available_project_runners"]' do
click_on 'Enable for this project'
end
- expect(page.find('.activated-specific-runners')).to have_content(project_runner.display_name)
+ expect(page.find('[data-testid="assigned_project_runners"]')).to have_content(project_runner.display_name)
- within '.activated-specific-runners' do
+ within '[data-testid="assigned_project_runners"]' do
click_on 'Disable for this project'
end
- expect(page.find('.available-specific-runners')).to have_content(project_runner.display_name)
+ expect(page.find('[data-testid="available_project_runners"]')).to have_content(project_runner.display_name)
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 dd7095107f4..b7d06a3a962 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -4,241 +4,221 @@ require 'spec_helper'
RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
+ include ListboxHelpers
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- context 'when signed in' do
+ context 'when signed in' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context 'when on a project page' do
before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- project.add_maintainer(user)
- sign_in(user)
+ visit(project_path(project))
end
- context 'when on a project page' do
- before do
- visit(project_path(project))
- end
-
- it 'finds a file' do
- submit_search('application.js')
- select_search_scope('Code')
+ it 'finds a file' do
+ submit_search('application.js')
+ select_search_scope('Code')
- expect(page).to have_selector('.results', text: 'application.js')
- expect(page).to have_selector('.file-content .code')
- expect(page).to have_selector("span.line[lang='javascript']")
- expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
- expect(page).to have_button('Copy file path')
- end
+ expect(page).to have_selector('.results', text: 'application.js')
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
+ expect(page).to have_button('Copy file path')
end
+ end
- context 'when on a project search page' do
- before do
- visit(search_path)
- find('[data-testid="project-filter"]').click
+ context 'when on a project search page' do
+ before do
+ visit(search_path)
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
end
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'blobs' do
- let(:additional_params) { { project_id: project.id } }
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'blobs' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- context 'when searching code' do
- let(:expected_result) { 'Update capybara, rspec-rails, poltergeist to recent versions' }
+ context 'when searching code' do
+ let(:expected_result) { 'Update capybara, rspec-rails, poltergeist to recent versions' }
- before do
- fill_in('dashboard_search', with: 'rspec')
- find('.gl-search-box-by-click-search-button').click
- end
+ before do
+ fill_in('dashboard_search', with: 'rspec')
+ find('.gl-search-box-by-click-search-button').click
+ end
- it 'finds code and links to blob' do
- expect(page).to have_selector('.results', text: expected_result)
+ it 'finds code and links to blob' do
+ expect(page).to have_selector('.results', text: expected_result)
- find("#blob-L3").click
- expect(current_url).to match(%r{blob/master/.gitignore#L3})
- end
+ find("#blob-L3").click
+ expect(current_url).to match(%r{blob/master/.gitignore#L3})
+ end
- it 'finds code and links to blame' do
- expect(page).to have_selector('.results', text: expected_result)
+ it 'finds code and links to blame' do
+ expect(page).to have_selector('.results', text: expected_result)
- find("#blame-L3").click
- expect(current_url).to match(%r{blame/master/.gitignore#L3})
- end
+ find("#blame-L3").click
+ expect(current_url).to match(%r{blame/master/.gitignore#L3})
+ end
- it_behaves_like 'code highlight' do
- subject { page }
- end
+ it_behaves_like 'code highlight' do
+ subject { page }
end
+ end
- it 'search multiple words with refs switching' do
- expected_result = 'Use `snake_case` for naming files'
- search = 'for naming files'
+ it 'search multiple words with refs switching' do
+ expected_result = 'Use `snake_case` for naming files'
+ search = 'for naming files'
+ ref_selector = 'v1.0.0'
- fill_in('dashboard_search', with: search)
- find('.gl-search-box-by-click-search-button').click
+ fill_in('dashboard_search', with: search)
+ find('.gl-search-box-by-click-search-button').click
- expect(page).to have_selector('.results', text: expected_result)
+ expect(page).to have_selector('.results', text: expected_result)
- find('.ref-selector').click
- wait_for_requests
+ click_button 'master'
+ wait_for_requests
- page.within('.ref-selector') do
- find('li', text: 'v1.0.0').click
- end
+ select_listbox_item(ref_selector)
- expect(page).to have_selector('.results', text: expected_result)
+ expect(page).to have_selector('.results', text: expected_result)
- expect(find_field('dashboard_search').value).to eq(search)
- expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- end
+ expect(find_field('dashboard_search').value).to eq(search)
+ expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
+ expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
end
+ end
- context 'when :new_header_search is true' do
- context 'search code within refs' do
- let(:ref_name) { 'v1.0.0' }
+ context 'when :new_header_search is true' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- before do
- # This feature is disabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: true)
- visit(project_tree_path(project, ref_name))
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: true)
+ visit(project_tree_path(project, ref_name))
- submit_search('gitlab-grack')
- select_search_scope('Code')
- end
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
+ end
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- it 'persists branch name across search' do
- find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- # this example is use to test the design that the refs is not
- # only represent the branch as well as the tags.
- it 'ref switcher list all the branches and tags' do
- find('.ref-selector').click
- wait_for_requests
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
+ wait_for_requests
- page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
- end
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
end
+ end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
-
- find('.ref-selector').click
- wait_for_requests
+ it 'search result changes when refs switched' do
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
- wait_for_requests
+ find('.ref-selector').click
+ wait_for_requests
- find('li', text: ref).click
- end
+ select_listbox_item('add-ipython-files')
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
end
+ end
- context 'when :new_header_search is false' do
- context 'search code within refs' do
- let(:ref_name) { 'v1.0.0' }
+ context 'when :new_header_search is false' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- before do
- # This feature is disabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: false)
- visit(project_tree_path(project, ref_name))
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: false)
+ visit(project_tree_path(project, ref_name))
- submit_search('gitlab-grack')
- select_search_scope('Code')
- end
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
+ end
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- it 'persists branch name across search' do
- find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- # this example is use to test the design that the refs is not
- # only represent the branch as well as the tags.
- it 'ref switcher list all the branches and tags' do
- find('.ref-selector').click
- wait_for_requests
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
+ wait_for_requests
- page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
- end
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
end
+ end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
-
- find('.ref-selector').click
- wait_for_requests
+ it 'search result changes when refs switched' do
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
- wait_for_requests
+ find('.ref-selector').click
+ wait_for_requests
- find('li', text: ref).click
- end
+ select_listbox_item('add-ipython-files')
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
end
end
+ end
- it 'no ref switcher shown in issue result summary' do
- issue = create(:issue, title: 'test', project: project)
- visit(project_tree_path(project))
+ it 'no ref switcher shown in issue result summary' do
+ issue = create(:issue, title: 'test', project: project)
+ visit(project_tree_path(project))
- submit_search('test')
- select_search_scope('Code')
+ submit_search('test')
+ select_search_scope('Code')
- expect(page).to have_selector('.ref-selector')
+ expect(page).to have_selector('.ref-selector')
- select_search_scope('Issues')
+ select_search_scope('Issues')
- expect(find(:css, '.results')).to have_link(issue.title)
- expect(page).not_to have_selector('.ref-selector')
- end
+ expect(find(:css, '.results')).to have_link(issue.title)
+ expect(page).not_to have_selector('.ref-selector')
end
+ end
- context 'when signed out' do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- end
-
- context 'when block_anonymous_global_searches is enabled' do
- it 'is redirected to login page' do
- visit(search_path)
+ context 'when signed out' do
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
- expect(page).to have_content('You must be logged in to search across all of GitLab')
- end
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
end
end
end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index d7f6143d173..f7af1797c71 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -3,51 +3,45 @@
require 'spec_helper'
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) }
let_it_be(:user) { create(:user) }
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- project.add_reporter(user)
- sign_in(user)
+ before do
+ project.add_reporter(user)
+ sign_in(user)
- visit(project_path(project))
- end
+ visit(project_path(project))
+ end
- include_examples 'search timeouts', 'notes' do
- let(:additional_params) { { project_id: project.id } }
- end
+ include_examples 'search timeouts', 'notes' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- context 'when a comment is in commits' do
- context 'when comment belongs to an invalid commit' do
- let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
- it 'finds a commit' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ it 'finds a commit' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
- page.within('.results') do
- expect(page).to have_content('Commit deleted')
- expect(page).to have_content('12345678')
- end
+ page.within('.results') do
+ expect(page).to have_content('Commit deleted')
+ expect(page).to have_content('12345678')
end
end
end
+ end
- context 'when a comment is in a snippet' do
- let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
- let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
- it 'finds a snippet' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ it 'finds a snippet' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
- expect(page).to have_selector('.results', text: snippet.title)
- end
+ expect(page).to have_selector('.results', text: snippet.title)
end
end
end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index 1fd62a01c78..724daf9277d 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -3,61 +3,55 @@
require 'spec_helper'
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) }
+ let_it_be(:project) { create(:project, :repository) }
- let(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- project.add_reporter(user)
- sign_in(user)
+ before do
+ project.add_reporter(user)
+ sign_in(user)
- visit(search_path(project_id: project.id))
- end
+ visit(search_path(project_id: project.id))
+ end
- include_examples 'search timeouts', 'commits' do
- let(:additional_params) { { project_id: project.id } }
- end
+ include_examples 'search timeouts', 'commits' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- context 'when searching by SHA' do
- it 'finds a commit and redirects to its page' do
- submit_search(sha)
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ submit_search(sha)
- expect(page).to have_current_path(project_commit_path(project, sha))
- end
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
- it 'finds a commit in uppercase and redirects to its page' do
- submit_search(sha.upcase)
+ it 'finds a commit in uppercase and redirects to its page' do
+ submit_search(sha.upcase)
- expect(page).to have_current_path(project_commit_path(project, sha))
- end
+ expect(page).to have_current_path(project_commit_path(project, sha))
end
+ end
- context 'when searching by message' do
- it 'finds a commit and holds on /search page' do
- project.repository.commit_files(
- user,
- message: 'Message referencing another sha: "deadbeef"',
- branch_name: 'master',
- actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
- )
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ project.repository.commit_files(
+ user,
+ message: 'Message referencing another sha: "deadbeef"',
+ branch_name: 'master',
+ actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
+ )
- submit_search('deadbeef')
+ submit_search('deadbeef')
- expect(page).to have_current_path('/search', ignore_query: true)
- end
+ expect(page).to have_current_path('/search', ignore_query: true)
+ end
- it 'finds multiple commits' do
- submit_search('See merge request')
- select_search_scope('Commits')
+ it 'finds multiple commits' do
+ submit_search('See merge request')
+ select_search_scope('Commits')
- expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
- end
+ expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
end
end
end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 6ebbe86d1a9..9451e337db1 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
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) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
@@ -17,133 +15,123 @@ RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limitin
select_search_scope('Issues')
end
- where(search_page_vertical_nav_enabled: [true, false])
-
- with_them do
- context 'when signed in' do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ context 'when signed in' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
- project.add_maintainer(user)
- sign_in(user)
+ visit(search_path)
+ end
- visit(search_path)
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'issues'
- include_examples 'top right search form'
- include_examples 'search timeouts', 'issues'
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
- it 'finds an issue' do
- search_for_issue(issue1.title)
-
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
- end
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
end
+ end
- it 'hides confidential icon for non-confidential issues' do
- search_for_issue(issue1.title)
+ it 'hides confidential icon for non-confidential issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
- end
+ page.within('.results') do
+ expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
end
+ end
- it 'shows confidential icon for confidential issues' do
- search_for_issue(issue2.title)
+ it 'shows confidential icon for confidential issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page).to have_css('[data-testid="eye-slash-icon"]')
- end
+ page.within('.results') do
+ expect(page).to have_css('[data-testid="eye-slash-icon"]')
end
+ end
- it 'shows correct badge for open issues' do
- search_for_issue(issue1.title)
+ it 'shows correct badge for open issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_css('.badge-success')
- expect(page).not_to have_css('.badge-info')
- end
+ page.within('.results') do
+ expect(page).to have_css('.badge-success')
+ expect(page).not_to have_css('.badge-info')
end
+ end
- it 'shows correct badge for closed issues' do
- search_for_issue(issue2.title)
+ it 'shows correct badge for closed issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page).not_to have_css('.badge-success')
- expect(page).to have_css('.badge-info')
- end
+ page.within('.results') do
+ expect(page).not_to have_css('.badge-success')
+ expect(page).to have_css('.badge-info')
end
+ end
- it 'sorts by created date' do
- search_for_issue('issue')
+ it 'sorts by created date' do
+ search_for_issue('issue')
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue2.title)
- expect(page.all('.search-result-row').last).to have_link(issue1.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(issue2.title)
+ expect(page.all('.search-result-row').last).to have_link(issue1.title)
+ end
- find('[data-testid="sort-highest-icon"]').click
+ find('[data-testid="sort-highest-icon"]').click
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue1.title)
- expect(page.all('.search-result-row').last).to have_link(issue2.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(issue1.title)
+ expect(page.all('.search-result-row').last).to have_link(issue2.title)
end
+ end
- context 'when on a project page' do
- it 'finds an issue' do
- find('[data-testid="project-filter"]').click
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- search_for_issue(issue1.title)
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
- end
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
end
end
end
+ end
- context 'when signed out' do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- end
-
- context 'when block_anonymous_global_searches is disabled' do
- let_it_be(:project) { create(:project, :public) }
+ context 'when signed out' do
+ context 'when block_anonymous_global_searches is disabled' do
+ let_it_be(:project) { create(:project, :public) }
- before do
- stub_feature_flags(block_anonymous_global_searches: false)
+ before do
+ stub_feature_flags(block_anonymous_global_searches: false)
- visit(search_path)
- end
+ visit(search_path)
+ end
- include_examples 'top right search form'
+ include_examples 'top right search form'
- it 'finds an issue' do
- search_for_issue(issue1.title)
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
- end
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
end
end
+ end
- context 'when block_anonymous_global_searches is enabled' do
- it 'is redirected to login page' do
- visit(search_path)
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
- expect(page).to have_content('You must be logged in to search across all of GitLab')
- end
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
end
end
end
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 69f62a4c1e2..d7b52d9e07a 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -3,12 +3,10 @@
require 'spec_helper'
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) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
- let!(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+ let_it_be(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
+ let_it_be(:merge_request2) { create(:merge_request, :simple, title: 'Merge Request Bar', source_project: project, target_project: project) }
def search_for_mr(search)
fill_in('dashboard_search', with: search)
@@ -16,64 +14,60 @@ RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate
select_search_scope('Merge requests')
end
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- sign_in(user)
+ before do
+ sign_in(user)
- visit(search_path)
- end
+ visit(search_path)
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'merge_requests'
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'merge_requests'
- it 'finds a merge request' do
- search_for_mr(merge_request1.title)
+ it 'finds a merge request' do
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
- # Each result should have MR refs like `gitlab-org/gitlab!1`
- page.all('.search-result-row').each do |e|
- expect(e.text).to match(/!\d+/)
- end
+ # Each result should have MR refs like `gitlab-org/gitlab!1`
+ page.all('.search-result-row').each do |e|
+ expect(e.text).to match(/!\d+/)
end
end
+ end
- it 'sorts by created date' do
- search_for_mr('Merge Request')
+ it 'sorts by created date' do
+ search_for_mr('Merge Request')
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
+ end
- find('[data-testid="sort-highest-icon"]').click
+ find('[data-testid="sort-highest-icon"]').click
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
end
+ end
- context 'when on a project page' do
- it 'finds a merge request' do
- find('[data-testid="project-filter"]').click
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- search_for_mr(merge_request1.title)
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
- end
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
end
end
end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index e87c2176380..238e59be940 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -4,29 +4,42 @@ require 'spec_helper'
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) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
+ let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let_it_be(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
- let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
- let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+ visit(search_path)
+ end
- where(search_page_vertical_nav_enabled: [true, false])
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'milestones'
- with_them do
- before do
- project.add_maintainer(user)
- sign_in(user)
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ it 'finds a milestone' do
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Milestones')
- visit(search_path)
+ page.within('.results') do
+ expect(page).to have_link(milestone1.title)
+ expect(page).not_to have_link(milestone2.title)
end
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'milestones'
-
+ context 'when on a project page' do
it 'finds a milestone' do
+ find('[data-testid="project-filter"]').click
+
+ wait_for_requests
+
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
+
fill_in('dashboard_search', with: milestone1.title)
find('.gl-search-box-by-click-search-button').click
select_search_scope('Milestones')
@@ -36,26 +49,5 @@ feature_category: :global_search do
expect(page).not_to have_link(milestone2.title)
end
end
-
- context 'when on a project page' do
- it 'finds a milestone' do
- find('[data-testid="project-filter"]').click
-
- wait_for_requests
-
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
-
- fill_in('dashboard_search', with: milestone1.title)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Milestones')
-
- page.within('.results') do
- expect(page).to have_link(milestone1.title)
- expect(page).not_to have_link(milestone2.title)
- end
- end
- end
end
end
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index 4737cef98c7..e0a07c5103d 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -7,85 +7,80 @@ RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting
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') }
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
-
- sign_in(user1)
- end
+ before do
+ sign_in(user1)
+ end
- include_examples 'search timeouts', 'users' do
- before do
- visit(search_path)
- end
+ include_examples 'search timeouts', 'users' do
+ before do
+ visit(search_path)
end
+ end
- context 'when on the dashboard' do
- it 'finds the user' do
- visit dashboard_projects_path
+ context 'when on the dashboard' do
+ it 'finds the user' do
+ visit dashboard_projects_path
- submit_search('gob')
- select_search_scope('Users')
+ submit_search('gob')
+ select_search_scope('Users')
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
- end
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
end
end
+ end
- context 'when on the project page' do
- let_it_be_with_reload(:project) { create(:project) }
+ context 'when on the project page' do
+ let_it_be_with_reload(:project) { create(:project) }
- before do
- project.add_developer(user1)
- project.add_developer(user2)
- end
+ before do
+ project.add_developer(user1)
+ project.add_developer(user2)
+ end
- it 'finds the user belonging to the project' do
- visit project_path(project)
+ it 'finds the user belonging to the project' do
+ visit project_path(project)
- submit_search('gob')
- select_search_scope('Users')
+ submit_search('gob')
+ select_search_scope('Users')
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
- end
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
end
end
+ end
- context 'when on the group page' do
- let(:group) { create(:group) }
+ context 'when on the group page' do
+ let(:group) { create(:group) }
- before do
- group.add_developer(user1)
- group.add_developer(user2)
- end
+ before do
+ group.add_developer(user1)
+ group.add_developer(user2)
+ end
- it 'finds the user belonging to the group' do
- visit group_path(group)
+ it 'finds the user belonging to the group' do
+ visit group_path(group)
- submit_search('gob')
- select_search_scope('Users')
+ submit_search('gob')
+ select_search_scope('Users')
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
- end
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
end
end
end
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 c7dc3e34bb7..1d8bdc58ce6 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -4,58 +4,53 @@ require 'spec_helper'
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) }
+ let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
+ let_it_be(:wiki_page) do
+ create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content')
+ end
- let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
- let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content') }
-
- where(search_page_vertical_nav_enabled: [true, false])
- with_them do
- before do
- stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- project.add_maintainer(user)
- sign_in(user)
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
- visit(search_path)
- end
+ visit(search_path)
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'wiki_blobs' do
- let(:additional_params) { { project_id: project.id } }
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'wiki_blobs' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- shared_examples 'search wiki blobs' do
- it 'finds a page' do
- find('[data-testid="project-filter"]').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- fill_in('dashboard_search', with: search_term)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Wiki')
+ fill_in('dashboard_search', with: search_term)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Wiki')
- page.within('.results') do
- expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
- end
+ page.within('.results') do
+ expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
end
end
+ end
- context 'when searching by content' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'content' }
- end
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
end
+ end
- context 'when searching by title' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'title' }
- end
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'title' }
end
end
end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index bc82afc70a3..5d9b451cdf6 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, ref)
- expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
+ expect(page).to have_selector('.gl-badge', text: 'Unverified')
# user changes their email which makes the gpg key verified
perform_enqueued_jobs do
@@ -26,7 +26,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, ref)
- expect(page).to have_selector('.gpg-status-box', text: 'Verified')
+ expect(page).to have_selector('.gl-badge', text: 'Verified')
end
it 'changes from unverified to verified when the user adds the missing gpg key', :sidekiq_might_not_need_inline do
@@ -35,7 +35,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, ref)
- expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
+ expect(page).to have_selector('.gl-badge', text: 'Unverified')
# user adds the gpg key which makes the signature valid
perform_enqueued_jobs do
@@ -44,7 +44,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, ref)
- expect(page).to have_selector('.gpg-status-box', text: 'Verified')
+ expect(page).to have_selector('.gl-badge', text: 'Verified')
end
context 'shows popover badges', :js do
@@ -75,7 +75,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Unverified').click
+ page.find('.gl-badge', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature.'
@@ -90,7 +90,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Unverified').click
+ page.find('.gl-badge', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not associated with the GPG Key.'
@@ -104,7 +104,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA)
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Unverified').click
+ page.find('.gl-badge', text: 'Unverified').click
within '.popover' do
expect(page).to have_content "This commit was signed with a different user's verified signature."
@@ -118,7 +118,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, GpgHelpers::MULTIPLE_SIGNATURES_SHA)
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Unverified').click
+ page.find('.gl-badge', text: 'Unverified').click
within '.popover' do
expect(page).to have_content "This commit was signed with multiple signatures."
@@ -131,7 +131,7 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Verified').click
+ page.find('.gl-badge', text: 'Verified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email was verified to belong to the same user.'
@@ -146,14 +146,14 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
wait_for_all_requests
# wait for the signature to get generated
- expect(page).to have_selector('.gpg-status-box', text: 'Verified')
+ expect(page).to have_selector('.gl-badge', text: 'Verified')
user_1.destroy!
refresh
wait_for_all_requests
- page.find('.gpg-status-box', text: 'Verified').click
+ page.find('.gl-badge', text: 'Verified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email was verified to belong to the same user.'
@@ -170,9 +170,9 @@ RSpec.describe 'GPG signed commits', feature_category: :source_code_management d
end
it 'displays commit signature' do
- expect(page).to have_selector('.gpg-status-box', text: 'Unverified')
+ expect(page).to have_selector('.gl-badge', text: 'Unverified')
- page.find('.gpg-status-box', text: 'Unverified').click
+ page.find('.gl-badge', text: 'Unverified').click
within '.popover' do
expect(page).to have_content 'This commit was signed with multiple signatures.'
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
index 5d49b36f4fe..0e3f96906de 100644
--- a/spec/features/snippets/spam_snippets_spec.rb
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://git
end
before do
- stub_feature_flags(allow_possible_spam: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!(
@@ -74,15 +73,15 @@ RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://git
end
end
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_application_setting(recaptcha_enabled: false)
- end
-
+ context 'when allow_possible_spam application setting is false' do
it_behaves_like 'does not allow creation'
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it_behaves_like 'solve reCAPTCHA'
end
end
@@ -94,7 +93,7 @@ RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://git
end
end
- context 'when allow_possible_spam feature flag is false' do
+ context 'when allow_possible_spam application setting is false' do
before do
stub_application_setting(recaptcha_enabled: false)
end
@@ -102,7 +101,11 @@ RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://git
it_behaves_like 'does not allow creation'
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it_behaves_like 'does not allow creation'
end
end
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
index 2ccdb68e844..dde2f0fcfaa 100644
--- a/spec/features/snippets_spec.rb
+++ b/spec/features/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Snippets', feature_category: :snippets do
+RSpec.describe 'Snippets', feature_category: :source_code_management 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 111710ba325..6a1db051e87 100644
--- a/spec/features/tags/developer_creates_tag_spec.rb
+++ b/spec/features/tags/developer_creates_tag_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
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)
+ expect(find('.gl-new-dropdown-inner')).not_to have_content(ref_name)
end
end
@@ -60,9 +60,9 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
page.within ref_row do
ref_input = find('[name="ref"]', visible: false)
expect(ref_input.value).to eq 'master'
- expect(find('.gl-dropdown-button-text')).to have_content 'master'
+ expect(find('.gl-button-text')).to have_content 'master'
find('.ref-selector').click
- expect(find('.dropdown-menu')).to have_content 'test'
+ expect(find('.gl-new-dropdown-inner')).to have_content 'test'
end
end
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index d35726fe125..8a9d2ff42d9 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -306,6 +306,12 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
describe 'commented tasks' do
let(:commented_tasks_markdown) do
<<-EOT.strip_heredoc
+ <!-- comment text -->
+
+ text
+
+ <!-- - [ ] commented out task -->
+
<!--
- [ ] a
-->
@@ -333,6 +339,41 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
expect(page).to have_selector('ul.task-list', count: 1)
expect(page).to have_selector('li.task-list-item', count: 1)
expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_content('1 of 1 checklist item completed')
+ end
+ end
+
+ describe 'tasks in code blocks' do
+ let(:code_tasks_markdown) do
+ <<-EOT.strip_heredoc
+ ```
+ - [ ] a
+ ```
+
+ - [ ] b
+ EOT
+ end
+
+ let!(:issue) { create(:issue, description: code_tasks_markdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+
+ find('.task-list-item-checkbox').click
+ wait_for_requests
+
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_content('1 of 1 checklist item completed')
end
end
@@ -370,6 +411,43 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do
end
end
+ describe 'summary properly formatted' do
+ let(:summary_markdown) do
+ <<-EOT.strip_heredoc
+ <details open>
+ <summary>Valid detail/summary with tasklist</summary>
+
+ - [ ] People Ops: do such and such
+
+ </details>
+
+ * [x] Task 1
+ EOT
+ end
+
+ let!(:issue) { create(:issue, description: summary_markdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 2)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+
+ first('.task-list-item-checkbox').click
+ wait_for_requests
+
+ visit_issue(project, issue)
+ wait_for_requests
+
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 2)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ expect(page).to have_content('2 of 2 checklist items completed')
+ end
+ end
+
describe 'markdown starting with new line character' do
let(:markdown_starting_with_new_line) do
<<-EOT.strip_heredoc
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 23a13994fa4..903211ec250 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -113,11 +113,24 @@ RSpec.describe 'Triggers', :js, feature_category: :continuous_integration do
end
end
+ it 'hides the token value and reveals when clicking the "reveal values" button', :aggregate_failures do
+ create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+ visit project_settings_ci_cd_path(@project)
+
+ expect(page.find('.triggers-list')).to have_content('*' * 47)
+
+ page.find('[data-testid="reveal-hide-values-button"]').click
+
+ expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token)
+ end
+
it 'do not show "Edit" or full token for not owned trigger' do
# Create trigger with user different from current_user
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
+ page.find('[data-testid="reveal-hide-values-button"]').click
+
aggregate_failures 'shows truncated token, no clipboard button and no edit link' do
expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
expect(page.find('.triggers-list')).not_to have_selector('[data-testid="clipboard-btn"]')
@@ -130,6 +143,8 @@ RSpec.describe 'Triggers', :js, feature_category: :continuous_integration do
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
+ page.find('[data-testid="reveal-hide-values-button"]').click
+
aggregate_failures 'shows full token, clipboard button and edit link' do
expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
expect(page.find('.triggers-list')).to have_selector('[data-testid="clipboard-btn"]')
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index bcab35335cb..23fa6261bd5 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_inline, feature_category: :not_owne
let_it_be(:recipient) { create(:user) }
let(:params) { { title: 'A bug!', description: 'Fix it!', assignee_ids: [recipient.id] } }
- let(:issue) { Issues::CreateService.new(project: project, current_user: author, params: params, spam_params: nil).execute[:issue] }
+ let(:issue) { Issues::CreateService.new(container: project, current_user: author, params: params, spam_params: nil).execute[:issue] }
let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
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 78cede77fea..02e98905662 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', feature_category: :users do
+RSpec.describe 'User uploads avatar to group', feature_category: :user_profile 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 fb62b5eadc5..f1023f17d3e 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', feature_category: :users do
+RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile do
let!(:user) { create(:user) }
let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
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 8c4e68c454f..ea39e5c5a49 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', feature_category: :users do
+RSpec.describe 'AdditionalEmailToExistingAccount', feature_category: :user_profile 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/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index de52f0b517e..481ff52b800 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
before do
stub_feature_flags(require_email_verification: require_email_verification_enabled)
+ stub_feature_flags(skip_require_email_verification: false)
end
shared_examples 'email verification required' do
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 489e7d61ff9..ff903358931 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, feature_category: :users do
+RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_profile do
let(:user) { create(:user) }
let(:contributed_project) { create(:project, :public, :repository) }
@@ -18,6 +18,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :users d
end
before do
+ stub_feature_flags(profile_tabs_vue: false)
sign_in user
end
diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb
index a2604cd298a..bc37c9941ce 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', feature_category: :users do
+RSpec.describe 'User RSS', feature_category: :user_profile 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 318dd688fa4..88b2d918976 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', feature_category: :users do
+RSpec.describe 'User page', feature_category: :user_profile 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>') }
@@ -16,10 +16,31 @@ RSpec.describe 'User page', feature_category: :users do
end
context 'with public profile' do
- it 'shows all the tabs' do
+ context 'with `profile_tabs_vue` feature flag disabled' do
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
+ it 'shows all the tabs' do
+ subject
+
+ page.within '.nav-links' do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Groups')
+ expect(page).to have_link('Contributed projects')
+ expect(page).to have_link('Personal projects')
+ expect(page).to have_link('Snippets')
+ expect(page).to have_link('Followers')
+ expect(page).to have_link('Following')
+ end
+ end
+ end
+
+ it 'shows all the tabs', :js do
subject
- page.within '.nav-links' do
+ page.within '[role="tablist"]' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
@@ -189,11 +210,33 @@ RSpec.describe 'User page', feature_category: :users do
expect(page).to have_content("This user has a private profile")
end
- it 'shows own tabs' do
+ context 'with `profile_tabs_vue` feature flag disabled' do
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
+ it 'shows own tabs' do
+ sign_in(user)
+ subject
+
+ page.within '.nav-links' do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Groups')
+ expect(page).to have_link('Contributed projects')
+ expect(page).to have_link('Personal projects')
+ expect(page).to have_link('Snippets')
+ expect(page).to have_link('Followers')
+ expect(page).to have_link('Following')
+ end
+ end
+ end
+
+ it 'shows own tabs', :js do
sign_in(user)
subject
- page.within '.nav-links' do
+ page.within '[role="tablist"]' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
@@ -341,6 +384,7 @@ RSpec.describe 'User page', feature_category: :users do
page.within '.navbar-gitlab' do
expect(page).to have_link('Sign in')
+ expect(page).not_to have_link('Register')
end
end
end
@@ -352,12 +396,17 @@ RSpec.describe 'User page', feature_category: :users do
subject
page.within '.navbar-gitlab' do
- expect(page).to have_link('Sign in / Register')
+ expect(page).to have_link(_('Sign in'), exact: true)
+ expect(page).to have_link(_('Register'), exact: true)
end
end
end
context 'most recent activity' do
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
it 'shows the most recent activity' do
subject
@@ -388,6 +437,10 @@ RSpec.describe 'User page', feature_category: :users do
context 'with a bot user' do
let_it_be(:user) { create(:user, user_type: :security_bot) }
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 1057ae48c7d..11ff318c346 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', feature_category: :users do
+RSpec.describe 'Signup', feature_category: :user_profile do
include TermsHelper
let(:new_user) { build_stubbed(:user) }
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index 20fc2981418..2876351be37 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -2,10 +2,14 @@
require 'spec_helper'
-RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :snippets do
+RSpec.describe 'Snippets tab on a user profile', :js, feature_category: :source_code_management do
context 'when the user has snippets' do
let(:user) { create(:user) }
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
context 'pagination' do
let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index 7d2137b81b8..5c61843e558 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, feature_category: :users do
+RSpec.describe 'Users > Terms', :js, feature_category: :user_profile 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 841b324fba4..52ca2397582 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
@@ -28,6 +28,10 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
end
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ end
+
it 'hides loading spinner after load', :js do
visit user_path(user)
click_nav_link('Personal projects')
@@ -125,7 +129,7 @@ RSpec.describe 'Users > User browses projects on user page', :js, feature_catego
end
before do
- Issues::CreateService.new(project: contributed_project, current_user: user, params: { title: 'Bug in old browser' }, spam_params: nil).execute
+ Issues::CreateService.new(container: contributed_project, current_user: user, params: { title: 'Bug in old browser' }, spam_params: nil).execute
event = create(:push_event, project: contributed_project, author: user)
create(:push_event_payload, event: event, commit_count: 3)
end
diff --git a/spec/features/users/zuora_csp_spec.rb b/spec/features/users/zuora_csp_spec.rb
deleted file mode 100644
index b07c923fa54..00000000000
--- a/spec/features/users/zuora_csp_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-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) }
-
- before do
- project.add_developer(user)
- sign_in(user)
- end
-
- it 'has proper Content Security Policy headers' do
- visit pipeline_path(pipeline)
-
- expect(response_headers['Content-Security-Policy']).to include('https://*.zuora.com')
- end
-end
diff --git a/spec/features/webauthn_spec.rb b/spec/features/webauthn_spec.rb
index e2f16f4a017..859793d1353 100644
--- a/spec/features/webauthn_spec.rb
+++ b/spec/features/webauthn_spec.rb
@@ -10,57 +10,62 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
WebAuthn.configuration.origin = app_id
end
- it_behaves_like 'hardware device for 2fa', 'WebAuthn'
-
- describe 'registration' do
- let(:user) { create(:user) }
-
+ context 'when the webauth_without_totp feature flag is disabled' do
before do
- gitlab_sign_in(user)
- user.update_attribute(:otp_required_for_login, true)
+ stub_feature_flags(webauthn_without_totp: false)
end
- describe 'when 2FA via OTP is enabled' do
- it 'allows registering more than one device' do
- visit profile_account_path
+ it_behaves_like 'hardware device for 2fa', 'WebAuthn'
+
+ describe 'registration' do
+ let(:user) { create(:user) }
+
+ before do
+ gitlab_sign_in(user)
+ user.update_attribute(:otp_required_for_login, true)
+ end
+
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows registering more than one device' do
+ visit profile_account_path
- # First device
+ # First device
+ manage_two_factor_authentication
+ first_device = register_webauthn_device
+ expect(page).to have_content('Your WebAuthn device was registered')
+
+ # Second device
+ second_device = register_webauthn_device(name: 'My other device')
+ expect(page).to have_content('Your WebAuthn device was registered')
+
+ expect(page).to have_content(first_device.name)
+ expect(page).to have_content(second_device.name)
+ expect(WebauthnRegistration.count).to eq(2)
+ end
+ end
+
+ it 'allows the same device to be registered for multiple users' do
+ # First user
+ visit profile_account_path
manage_two_factor_authentication
- first_device = register_webauthn_device
+ webauthn_device = register_webauthn_device
expect(page).to have_content('Your WebAuthn device was registered')
+ gitlab_sign_out
- # Second device
- second_device = register_webauthn_device(name: 'My other device')
+ # Second user
+ user = gitlab_sign_in(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ manage_two_factor_authentication
+ register_webauthn_device(webauthn_device, name: 'My other device')
expect(page).to have_content('Your WebAuthn device was registered')
- expect(page).to have_content(first_device.name)
- expect(page).to have_content(second_device.name)
expect(WebauthnRegistration.count).to eq(2)
end
- end
-
- it 'allows the same device to be registered for multiple users' do
- # First user
- visit profile_account_path
- manage_two_factor_authentication
- webauthn_device = register_webauthn_device
- expect(page).to have_content('Your WebAuthn device was registered')
- gitlab_sign_out
-
- # Second user
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- register_webauthn_device(webauthn_device, name: 'My other device')
- expect(page).to have_content('Your WebAuthn device was registered')
-
- expect(WebauthnRegistration.count).to eq(2)
- end
- context 'when there are form errors' do
- let(:mock_register_js) do
- <<~JS
+ context 'when there are form errors' do
+ let(:mock_register_js) do
+ <<~JS
const mockResponse = {
type: 'public-key',
id: '',
@@ -72,135 +77,136 @@ RSpec.describe 'Using WebAuthn Devices for Authentication', :js, feature_categor
getClientExtensionResults: () => {},
};
navigator.credentials.create = function(_) {return Promise.resolve(mockResponse);}
- JS
- end
+ JS
+ end
- it "doesn't register the device if there are errors" do
- visit profile_account_path
- manage_two_factor_authentication
+ it "doesn't register the device if there are errors" do
+ visit profile_account_path
+ manage_two_factor_authentication
- # Have the "webauthn device" respond with bad data
- page.execute_script(mock_register_js)
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
+ # Have the "webauthn device" respond with bad data
+ page.execute_script(mock_register_js)
+ click_on 'Set up new device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register device'
- expect(WebauthnRegistration.count).to eq(0)
- expect(page).to have_content('The form contains the following error')
- expect(page).to have_content('did not send a valid JSON response')
- end
+ expect(WebauthnRegistration.count).to eq(0)
+ expect(page).to have_content('The form contains the following error')
+ expect(page).to have_content('did not send a valid JSON response')
+ end
- it 'allows retrying registration' do
- visit profile_account_path
- manage_two_factor_authentication
+ it 'allows retrying registration' do
+ visit profile_account_path
+ manage_two_factor_authentication
- # Failed registration
- page.execute_script(mock_register_js)
- click_on 'Set up new device'
- expect(page).to have_content('Your device was successfully set up')
- click_on 'Register device'
- expect(page).to have_content('The form contains the following error')
+ # Failed registration
+ page.execute_script(mock_register_js)
+ click_on 'Set up new device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register device'
+ expect(page).to have_content('The form contains the following error')
- # Successful registration
- register_webauthn_device
+ # Successful registration
+ register_webauthn_device
- expect(page).to have_content('Your WebAuthn device was registered')
- expect(WebauthnRegistration.count).to eq(1)
+ expect(page).to have_content('Your WebAuthn device was registered')
+ expect(WebauthnRegistration.count).to eq(1)
+ end
end
end
- end
- describe 'authentication' do
- let(:otp_required_for_login) { true }
- let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
- let!(:webauthn_device) do
- add_webauthn_device(app_id, user)
- end
+ describe 'authentication' do
+ let(:otp_required_for_login) { true }
+ let(:user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ let!(:webauthn_device) do
+ add_webauthn_device(app_id, user)
+ end
- describe 'when 2FA via OTP is disabled' do
- let(:otp_required_for_login) { false }
+ describe 'when 2FA via OTP is disabled' do
+ let(:otp_required_for_login) { false }
- it 'allows logging in with the WebAuthn device' do
- gitlab_sign_in(user)
+ it 'allows logging in with the WebAuthn device' do
+ gitlab_sign_in(user)
- webauthn_device.respond_to_webauthn_authentication
+ webauthn_device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
+ expect(page).to have_css('.sign-out-link', visible: false)
+ end
end
- end
- describe 'when 2FA via OTP is enabled' do
- it 'allows logging in with the WebAuthn device' do
- gitlab_sign_in(user)
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows logging in with the WebAuthn device' do
+ gitlab_sign_in(user)
- webauthn_device.respond_to_webauthn_authentication
+ webauthn_device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
+ expect(page).to have_css('.sign-out-link', visible: false)
+ end
end
- end
- describe 'when a given WebAuthn device has already been registered by another user' do
- describe 'but not the current user' do
- let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
+ describe 'when a given WebAuthn device has already been registered by another user' do
+ describe 'but not the current user' do
+ let(:other_user) { create(:user, webauthn_xid: WebAuthn.generate_user_id, otp_required_for_login: otp_required_for_login) }
- it 'does not allow logging in with that particular device' do
- # Register other user with a different WebAuthn device
- other_device = add_webauthn_device(app_id, other_user)
+ it 'does not allow logging in with that particular device' do
+ # Register other user with a different WebAuthn device
+ other_device = add_webauthn_device(app_id, other_user)
- # Try authenticating user with the old WebAuthn device
- gitlab_sign_in(user)
- other_device.respond_to_webauthn_authentication
- expect(page).to have_content('Authentication via WebAuthn device failed')
+ # Try authenticating user with the old WebAuthn device
+ gitlab_sign_in(user)
+ other_device.respond_to_webauthn_authentication
+ expect(page).to have_content('Authentication via WebAuthn device failed')
+ end
end
- end
-
- describe "and also the current user" do
- # TODO Uncomment once WebAuthn::FakeClient supports passing credential options
- # (especially allow_credentials, as this is needed to specify which credential the
- # fake client should use. Currently, the first credential is always used).
- # There is an issue open for this: https://github.com/cedarcode/webauthn-ruby/issues/259
- it "allows logging in with that particular device" do
- pending("support for passing credential options in FakeClient")
- # Register current user with the same WebAuthn device
- current_user = gitlab_sign_in(:user)
- visit profile_account_path
- manage_two_factor_authentication
- register_webauthn_device(webauthn_device)
- gitlab_sign_out
- # Try authenticating user with the same WebAuthn device
- gitlab_sign_in(current_user)
- webauthn_device.respond_to_webauthn_authentication
-
- expect(page).to have_css('.sign-out-link', visible: false)
+ describe "and also the current user" do
+ # TODO Uncomment once WebAuthn::FakeClient supports passing credential options
+ # (especially allow_credentials, as this is needed to specify which credential the
+ # fake client should use. Currently, the first credential is always used).
+ # There is an issue open for this: https://github.com/cedarcode/webauthn-ruby/issues/259
+ it "allows logging in with that particular device" do
+ pending("support for passing credential options in FakeClient")
+ # Register current user with the same WebAuthn device
+ current_user = gitlab_sign_in(:user)
+ visit profile_account_path
+ manage_two_factor_authentication
+ register_webauthn_device(webauthn_device)
+ gitlab_sign_out
+
+ # Try authenticating user with the same WebAuthn device
+ gitlab_sign_in(current_user)
+ webauthn_device.respond_to_webauthn_authentication
+
+ expect(page).to have_css('.sign-out-link', visible: false)
+ end
end
end
- end
- describe 'when a given WebAuthn device has not been registered' do
- it 'does not allow logging in with that particular device' do
- unregistered_device = FakeWebauthnDevice.new(page, 'My device')
- gitlab_sign_in(user)
- unregistered_device.respond_to_webauthn_authentication
+ describe 'when a given WebAuthn device has not been registered' do
+ it 'does not allow logging in with that particular device' do
+ unregistered_device = FakeWebauthnDevice.new(page, 'My device')
+ gitlab_sign_in(user)
+ unregistered_device.respond_to_webauthn_authentication
- expect(page).to have_content('Authentication via WebAuthn device failed')
+ expect(page).to have_content('Authentication via WebAuthn device failed')
+ end
end
- end
- describe 'when more than one device has been registered by the same user' do
- it 'allows logging in with either device' do
- first_device = add_webauthn_device(app_id, user)
- second_device = add_webauthn_device(app_id, user)
+ describe 'when more than one device has been registered by the same user' do
+ it 'allows logging in with either device' do
+ first_device = add_webauthn_device(app_id, user)
+ second_device = add_webauthn_device(app_id, user)
- # Authenticate as both devices
- [first_device, second_device].each do |device|
- gitlab_sign_in(user)
- # register_webauthn_device(device)
- device.respond_to_webauthn_authentication
+ # Authenticate as both devices
+ [first_device, second_device].each do |device|
+ gitlab_sign_in(user)
+ # register_webauthn_device(device)
+ device.respond_to_webauthn_authentication
- expect(page).to have_css('.sign-out-link', visible: false)
+ expect(page).to have_css('.sign-out-link', visible: false)
- gitlab_sign_out
+ gitlab_sign_out
+ end
end
end
end
diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb
index 4403ca60d11..f41fb86d13c 100644
--- a/spec/features/work_items/work_item_children_spec.rb
+++ b/spec/features/work_items/work_item_children_spec.rb
@@ -31,15 +31,15 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
it 'toggles widget body', :aggregate_failures do
page.within('[data-testid="work-item-links"]') do
- expect(page).to have_selector('[data-testid="links-body"]')
+ expect(page).to have_selector('[data-testid="widget-body"]')
- click_button 'Collapse tasks'
+ click_button 'Collapse'
- expect(page).not_to have_selector('[data-testid="links-body"]')
+ expect(page).not_to have_selector('[data-testid="widget-body"]')
- click_button 'Expand tasks'
+ click_button 'Expand'
- expect(page).to have_selector('[data-testid="links-body"]')
+ expect(page).to have_selector('[data-testid="widget-body"]')
end
end
diff --git a/spec/features/work_items/work_item_spec.rb b/spec/features/work_items/work_item_spec.rb
index 577ec060020..3c71a27ff82 100644
--- a/spec/features/work_items/work_item_spec.rb
+++ b/spec/features/work_items/work_item_spec.rb
@@ -5,56 +5,33 @@ require 'spec_helper'
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)
visit project_work_items_path(project, work_items_path: work_item.id)
end
- context 'in work item description' do
- it 'shows GFM autocomplete', :aggregate_failures do
- click_button "Edit description"
-
- find('[aria-label="Description"]').send_keys("@#{user.username}")
-
- wait_for_requests
-
- page.within('.atwho-container') 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!")
+ it_behaves_like 'work items status'
+ it_behaves_like 'work items assignees'
+ it_behaves_like 'work items labels'
+ it_behaves_like 'work items comments'
+ it_behaves_like 'work items description'
+ end
- warning = 'Someone edited the description at the same time you did.'
- expect(page.find('[data-testid="work-item-description-conflicts"]')).to have_text(warning)
+ context 'for signed in owner' do
+ before do
+ project.add_owner(user)
- click_button "Save and overwrite"
+ sign_in(user)
- expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
- end
+ visit project_work_items_path(project, work_items_path: work_item.id)
end
+
+ it_behaves_like 'work items invite members'
end
end
diff --git a/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb b/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb
index 0275205028a..3e10ed78ab9 100644
--- a/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb
+++ b/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Analytics::CycleAnalytics::StageFinder do
let(:stage_id) { { id: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first } }
- subject { described_class.new(parent: project, stage_id: stage_id[:id]).execute }
+ subject { described_class.new(parent: project.project_namespace, stage_id: stage_id[:id]).execute }
context 'when looking up in-memory default stage by name exists' do
it { expect(subject).not_to be_persisted }
diff --git a/spec/finders/ci/freeze_periods_finder_spec.rb b/spec/finders/ci/freeze_periods_finder_spec.rb
index 6c58028a221..0aa73e698ed 100644
--- a/spec/finders/ci/freeze_periods_finder_spec.rb
+++ b/spec/finders/ci/freeze_periods_finder_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Ci::FreezePeriodsFinder, feature_category: :release_orchestration
project.add_developer(user)
end
- it_behaves_like 'returns nothing'
+ it_behaves_like 'returns freeze_periods ordered by created_at asc'
end
context 'when user is not a project member' do
diff --git a/spec/finders/ci/pipeline_schedules_finder_spec.rb b/spec/finders/ci/pipeline_schedules_finder_spec.rb
index 535c684289e..4699592b6d4 100644
--- a/spec/finders/ci/pipeline_schedules_finder_spec.rb
+++ b/spec/finders/ci/pipeline_schedules_finder_spec.rb
@@ -15,8 +15,39 @@ RSpec.describe Ci::PipelineSchedulesFinder do
let(:params) { { scope: nil } }
it 'selects all pipeline schedules' do
- expect(subject.count).to be(2)
- expect(subject).to include(active_schedule, inactive_schedule)
+ expect(subject).to contain_exactly(active_schedule, inactive_schedule)
+ end
+ end
+
+ context 'when the id is nil' do
+ let(:params) { { ids: nil } }
+
+ it 'selects all pipeline schedules' do
+ expect(subject).to contain_exactly(active_schedule, inactive_schedule)
+ end
+ end
+
+ context 'when the id is a single pipeline schedule' do
+ let(:params) { { ids: active_schedule.id } }
+
+ it 'selects one pipeline schedule' do
+ expect(subject).to contain_exactly(active_schedule)
+ end
+ end
+
+ context 'when multiple ids are provided' do
+ let(:params) { { ids: [active_schedule.id, inactive_schedule.id] } }
+
+ it 'selects multiple pipeline schedules' do
+ expect(subject).to contain_exactly(active_schedule, inactive_schedule)
+ end
+ end
+
+ context 'when multiple ids are provided and a scope is set' do
+ let(:params) { { scope: 'active', ids: [active_schedule.id, inactive_schedule.id] } }
+
+ it 'selects one pipeline schedule' do
+ expect(subject).to contain_exactly(active_schedule)
end
end
@@ -24,9 +55,7 @@ RSpec.describe Ci::PipelineSchedulesFinder do
let(:params) { { scope: 'active' } }
it 'selects only active pipelines' do
- expect(subject.count).to be(1)
- expect(subject).to include(active_schedule)
- expect(subject).not_to include(inactive_schedule)
+ expect(subject).to contain_exactly(active_schedule)
end
end
@@ -34,9 +63,7 @@ RSpec.describe Ci::PipelineSchedulesFinder do
let(:params) { { scope: 'inactive' } }
it 'selects only inactive pipelines' do
- expect(subject.count).to be(1)
- expect(subject).not_to include(active_schedule)
- expect(subject).to include(inactive_schedule)
+ expect(subject).to contain_exactly(inactive_schedule)
end
end
end
diff --git a/spec/finders/ci/pipelines_finder_spec.rb b/spec/finders/ci/pipelines_finder_spec.rb
index 9ce3becf013..8773fbccdfc 100644
--- a/spec/finders/ci/pipelines_finder_spec.rb
+++ b/spec/finders/ci/pipelines_finder_spec.rb
@@ -246,9 +246,9 @@ RSpec.describe Ci::PipelinesFinder 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' } }
+ let(:params) { { name: 'Build pipeline' } }
- it 'performs case insensitive compare' do
+ it 'performs exact compare' do
is_expected.to contain_exactly(pipeline)
end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 1aba77f4d6e..77260bb4c5c 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -60,8 +60,8 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet 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)
+ let_it_be(:runner_version_unavailable) do
+ create(:ci_runner_version, version: 'b', status: :unavailable)
end
let_it_be(:runner_version_available) do
@@ -77,7 +77,7 @@ RSpec.describe Ci::RunnersFinder, feature_category: :runner_fleet do
let(:upgrade_status) { status }
it "calls with_upgrade_status scope with corresponding :#{status} status" do
- if [:available, :not_available, :recommended].include?(status)
+ if [:available, :unavailable, :recommended].include?(status)
expected_result = Ci::Runner.with_upgrade_status(status)
end
diff --git a/spec/finders/concerns/finder_with_group_hierarchy_spec.rb b/spec/finders/concerns/finder_with_group_hierarchy_spec.rb
index 8c2026a00a1..c96e35372d6 100644
--- a/spec/finders/concerns/finder_with_group_hierarchy_spec.rb
+++ b/spec/finders/concerns/finder_with_group_hierarchy_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe FinderWithGroupHierarchy do
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:private_subgroup) { create(:group, :private, parent: private_group) }
- let(:user) { create(:user) }
+ let!(:user) { create(:user) }
context 'when specifying group' do
it 'returns only the group by default' do
@@ -109,4 +109,100 @@ RSpec.describe FinderWithGroupHierarchy do
expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id])
end
end
+
+ context 'with N+1 query check' do
+ def run_query(group)
+ finder_class
+ .new(user, group: group, include_descendant_groups: true)
+ .execute
+ .to_a
+
+ RequestStore.clear!
+ end
+
+ it 'does not produce N+1 query', :request_store do
+ private_group.add_developer(user)
+
+ run_query(private_subgroup) # warmup
+ control = ActiveRecord::QueryRecorder.new { run_query(private_subgroup) }
+
+ expect { run_query(private_group) }.not_to exceed_query_limit(control)
+ end
+ end
+
+ context 'when preload_max_access_levels_for_labels_finder is disabled' do
+ # All test cases were copied from above, these will be removed once the FF is removed.
+
+ before do
+ stub_feature_flags(preload_max_access_levels_for_labels_finder: false)
+ end
+
+ context 'when specifying group' do
+ it 'returns only the group by default' do
+ finder = finder_class.new(user, group: group)
+
+ expect(finder.execute).to match_array([group.id])
+ end
+ end
+
+ context 'when specifying group_id' do
+ it 'returns only the group by default' do
+ finder = finder_class.new(user, group_id: group.id)
+
+ expect(finder.execute).to match_array([group.id])
+ end
+ end
+
+ context 'when including items from group ancestors' do
+ before do
+ private_subgroup.add_developer(user)
+ end
+
+ it 'returns group and its ancestors' do
+ private_group.add_developer(user)
+
+ finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true)
+
+ expect(finder.execute).to match_array([private_group.id, private_subgroup.id])
+ end
+
+ it 'ignores groups which user can not read' do
+ finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true)
+
+ expect(finder.execute).to match_array([private_subgroup.id])
+ end
+
+ it 'returns them all when skip_authorization is true' do
+ finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true)
+
+ expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id])
+ end
+ end
+
+ context 'when including items from group descendants' do
+ before do
+ private_subgroup.add_developer(user)
+ end
+
+ it 'returns items from group and its descendants' do
+ private_group.add_developer(user)
+
+ finder = finder_class.new(user, group: private_group, include_descendant_groups: true)
+
+ expect(finder.execute).to match_array([private_group.id, private_subgroup.id])
+ end
+
+ it 'ignores items from groups which user can not read' do
+ finder = finder_class.new(user, group: private_group, include_descendant_groups: true)
+
+ expect(finder.execute).to match_array([private_subgroup.id])
+ end
+
+ it 'returns them all when skip_authorization is true' do
+ finder = finder_class.new(user, group: private_group, include_descendant_groups: true)
+
+ expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id])
+ end
+ end
+ end
end
diff --git a/spec/finders/fork_targets_finder_spec.rb b/spec/finders/fork_targets_finder_spec.rb
index 1acc38bb492..41651513f18 100644
--- a/spec/finders/fork_targets_finder_spec.rb
+++ b/spec/finders/fork_targets_finder_spec.rb
@@ -55,19 +55,5 @@ RSpec.describe ForkTargetsFinder do
expect(finder.execute(search: maintained_group.path)).to eq([maintained_group])
end
end
-
- context 'when searchable_fork_targets feature flag is disabled' do
- before do
- stub_feature_flags(searchable_fork_targets: false)
- end
-
- it_behaves_like 'returns namespaces and groups'
-
- context 'when search is provided' do
- it 'ignores the param and returns all user manageable namespaces' do
- expect(finder.execute).to match_array([user.namespace, maintained_group, owned_group, project.namespace, developer_group])
- end
- end
- end
end
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 0d1b58e2636..4a5eb389906 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupMembersFinder, '#execute' do
+RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do
let_it_be(:group) { create(:group) }
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:sub_sub_group) { create(:group, parent: sub_group) }
@@ -12,9 +12,9 @@ RSpec.describe GroupMembersFinder, '#execute' do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:user4) { create(:user) }
- let_it_be(:user5) { create(:user, :two_factor_via_otp) }
+ let_it_be(:user5_2fa) { create(:user, :two_factor_via_otp) }
- let!(:link) do
+ let_it_be(:link) do
create(:group_group_link, shared_group: group, shared_with_group: public_shared_group)
create(:group_group_link, shared_group: sub_group, shared_with_group: private_shared_group)
end
@@ -30,7 +30,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
end
context 'relations' do
- let!(:members) do
+ let_it_be(:members) do
{
user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
@@ -52,7 +52,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now),
user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4),
user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4),
- user5_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user5)
+ user5_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user5_2fa)
}
end
@@ -98,35 +98,31 @@ RSpec.describe GroupMembersFinder, '#execute' do
end
context 'search' do
- it 'returns searched members if requested' do
+ before_all do
group.add_maintainer(user2)
group.add_developer(user3)
- member = group.add_maintainer(user1)
+ end
+
+ let_it_be(:maintainer1) { group.add_maintainer(user1) }
+ it 'returns searched members if requested' do
result = described_class.new(group, params: { search: user1.name }).execute
- expect(result.to_a).to match_array([member])
+ expect(result.to_a).to match_array([maintainer1])
end
it 'returns nothing if search only in inherited relation' do
- group.add_maintainer(user2)
- group.add_developer(user3)
- group.add_maintainer(user1)
-
result = described_class.new(group, params: { search: user1.name }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([])
end
it 'returns searched member only from sub_group if search only in inherited relation' do
- group.add_maintainer(user2)
- group.add_developer(user3)
sub_group.add_maintainer(create(:user, name: user1.name))
- member = group.add_maintainer(user1)
- result = described_class.new(sub_group, params: { search: member.user.name }).execute(include_relations: [:inherited])
+ result = described_class.new(sub_group, params: { search: maintainer1.user.name }).execute(include_relations: [:inherited])
- expect(result.to_a).to contain_exactly(member)
+ expect(result.to_a).to contain_exactly(maintainer1)
end
end
@@ -134,7 +130,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns members with two-factor auth if requested by owner' do
group.add_owner(user2)
group.add_maintainer(user1)
- member = group.add_maintainer(user5)
+ member = group.add_maintainer(user5_2fa)
result = described_class.new(group, user2, params: { two_factor: 'enabled' }).execute
@@ -144,7 +140,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns members without two-factor auth if requested by owner' do
member1 = group.add_owner(user2)
member2 = group.add_maintainer(user1)
- member_with_2fa = group.add_maintainer(user5)
+ member_with_2fa = group.add_maintainer(user5_2fa)
result = described_class.new(group, user2, params: { two_factor: 'disabled' }).execute
@@ -156,7 +152,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
group.add_owner(user1)
group.add_maintainer(user2)
sub_group.add_maintainer(user3)
- member_with_2fa = sub_group.add_maintainer(user5)
+ member_with_2fa = sub_group.add_maintainer(user5_2fa)
result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct])
@@ -165,7 +161,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members with two-factor auth if requested by owner' do
group.add_owner(user1)
- member_with_2fa = group.add_maintainer(user5)
+ member_with_2fa = group.add_maintainer(user5_2fa)
sub_group.add_maintainer(user2)
sub_group.add_maintainer(user3)
@@ -178,7 +174,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
group.add_owner(user1)
group.add_maintainer(user2)
member3 = sub_group.add_maintainer(user3)
- sub_group.add_maintainer(user5)
+ sub_group.add_maintainer(user5_2fa)
result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct])
@@ -187,7 +183,7 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members without two-factor auth if requested by owner' do
member1 = group.add_owner(user1)
- group.add_maintainer(user5)
+ group.add_maintainer(user5_2fa)
sub_group.add_maintainer(user2)
sub_group.add_maintainer(user3)
@@ -198,10 +194,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
end
context 'filter by access levels' do
- let!(:owner1) { group.add_owner(user2) }
- let!(:owner2) { group.add_owner(user3) }
- let!(:maintainer1) { group.add_maintainer(user4) }
- let!(:maintainer2) { group.add_maintainer(user5) }
+ let_it_be(:owner1) { group.add_owner(user2) }
+ let_it_be(:owner2) { group.add_owner(user3) }
+ let_it_be(:maintainer1) { group.add_maintainer(user4) }
+ let_it_be(:maintainer2) { group.add_maintainer(user5_2fa) }
subject(:by_access_levels) { described_class.new(group, user1, params: { access_levels: access_levels }).execute }
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index 123df418f8d..25f9331005d 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -262,6 +262,18 @@ RSpec.describe GroupsFinder do
end
end
+ context 'with filter_group_ids' do
+ let_it_be(:group_one) { create(:group, :public, name: 'group_one') }
+ let_it_be(:group_two) { create(:group, :public, name: 'group_two') }
+ let_it_be(:group_three) { create(:group, :public, name: 'group_three') }
+
+ subject { described_class.new(user, { filter_group_ids: [group_one.id, group_three.id] }).execute }
+
+ it 'returns only the groups listed in the filter' do
+ is_expected.to contain_exactly(group_one, group_three)
+ end
+ end
+
context 'with include_ancestors' do
let_it_be(:user) { create(:user) }
diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb
index bf735152d99..4b6c729dab7 100644
--- a/spec/finders/merge_request_target_project_finder_spec.rb
+++ b/spec/finders/merge_request_target_project_finder_spec.rb
@@ -85,4 +85,28 @@ RSpec.describe MergeRequestTargetProjectFinder do
expect(finder.execute).to contain_exactly(other_fork, base_project)
end
end
+
+ context 'searching' do
+ let(:base_project) { create(:project, :private, path: 'base') }
+ let(:forked_project) { fork_project(base_project, base_project.first_owner) }
+ let(:other_fork) { fork_project(base_project) }
+
+ before do
+ base_project.add_developer(user)
+ forked_project.add_developer(user)
+ other_fork.add_developer(user)
+ end
+
+ it 'returns all projects with empty search' do
+ expect(finder.execute(search: '')).to match_array([base_project, forked_project, other_fork])
+ end
+
+ it 'returns forked project with search string' do
+ expect(finder.execute(search: forked_project.full_path)).to match_array([forked_project])
+ end
+
+ it 'returns no projects with search for project that does no exist' do
+ expect(finder.execute(search: 'root')).to match_array([])
+ end
+ end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index e58ec0cd59e..e8099924638 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -774,28 +774,10 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do
let(:params) { { project_id: project1.id, search: 'tanuki' } }
- context 'with anonymous user' do
- let(:merge_requests) { described_class.new(nil, params).execute }
-
- context 'with disable_anonymous_search feature flag enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'does not perform search' do
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request6)
- end
- end
-
- context 'with disable_anonymous_search feature flag disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
+ it 'returns matching merge requests' do
+ merge_requests = described_class.new(user, params).execute
- it 'returns matching merge requests' do
- expect(merge_requests).to contain_exactly(merge_request6)
- end
- end
+ expect(merge_requests).to contain_exactly(merge_request6)
end
end
end
diff --git a/spec/finders/namespaces/projects_finder_spec.rb b/spec/finders/namespaces/projects_finder_spec.rb
index 0f48aa6a9f4..040cdf33b87 100644
--- a/spec/finders/namespaces/projects_finder_spec.rb
+++ b/spec/finders/namespaces/projects_finder_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe Namespaces::ProjectsFinder do
let_it_be(:subgroup) { create(:group, parent: namespace) }
let_it_be(:project_1) { create(:project, :public, group: namespace, path: 'project', name: 'Project') }
let_it_be(:project_2) { create(:project, :public, group: namespace, path: 'test-project', name: 'Test Project') }
- let_it_be(:project_3) { create(:project, :public, path: 'sub-test-project', group: subgroup, name: 'Sub Test Project') }
- let_it_be(:project_4) { create(:project, :public, path: 'test-project-2', group: namespace, name: 'Test Project 2') }
+ let_it_be(:project_3) { create(:project, :public, :issues_disabled, path: 'sub-test-project', group: subgroup, name: 'Sub Test Project') }
+ let_it_be(:project_4) { create(:project, :public, :merge_requests_disabled, path: 'test-project-2', group: namespace, name: 'Test Project 2') }
let(:params) { {} }
@@ -55,6 +55,22 @@ RSpec.describe Namespaces::ProjectsFinder do
end
end
+ context 'when with_issues_enabled is true' do
+ let(:params) { { with_issues_enabled: true, include_subgroups: true } }
+
+ it 'returns the projects that have issues enabled' do
+ expect(projects).to contain_exactly(project_1, project_2, project_4)
+ end
+ end
+
+ context 'when with_merge_requests_enabled is true' do
+ let(:params) { { with_merge_requests_enabled: true } }
+
+ it 'returns the projects that have merge requests enabled' do
+ expect(projects).to contain_exactly(project_1, project_2)
+ end
+ end
+
context 'when sort is similarity' do
let(:params) { { sort: :similarity, search: 'test' } }
@@ -78,6 +94,20 @@ RSpec.describe Namespaces::ProjectsFinder do
expect(projects).to contain_exactly(project_2, project_4)
end
end
+
+ context 'when sort parameter is ACTIVITY_DESC' do
+ let(:params) { { sort: :latest_activity_desc } }
+
+ before do
+ project_2.update!(last_activity_at: 10.minutes.ago)
+ project_1.update!(last_activity_at: 5.minutes.ago)
+ project_4.update!(last_activity_at: 1.minute.ago)
+ end
+
+ it 'returns projects sorted by latest activity' do
+ expect(projects).to eq([project_4, project_1, project_2])
+ end
+ end
end
end
end
diff --git a/spec/finders/projects/ml/candidate_finder_spec.rb b/spec/finders/projects/ml/candidate_finder_spec.rb
new file mode 100644
index 00000000000..967d563c090
--- /dev/null
+++ b/spec/finders/projects/ml/candidate_finder_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::CandidateFinder, feature_category: :mlops do
+ let_it_be(:experiment) { create(:ml_experiments, user: nil) }
+
+ let_it_be(:candidates) do
+ %w[c a da b].zip([3, 2, 4, 1]).map do |name, auc|
+ make_candidate_and_metric(name, auc, experiment)
+ end
+ end
+
+ let_it_be(:another_candidate) { create(:ml_candidates) }
+ let_it_be(:first_candidate) { candidates.first }
+
+ let(:finder) { described_class.new(experiment, params) }
+ let(:page) { 1 }
+ let(:default_params) { { page: page } }
+ let(:params) { default_params }
+
+ subject { finder.execute }
+
+ describe '.execute' do
+ describe 'by name' do
+ context 'when params has no name' do
+ it 'fetches all candidates in the experiment' do
+ expect(subject).to match_array(candidates)
+ end
+
+ it 'does not fetch candidate not in experiment' do
+ expect(subject).not_to include(another_candidate)
+ end
+ end
+
+ context 'when name is included in params' do
+ let(:params) { { name: 'a' } }
+
+ it 'fetches the correct candidates' do
+ expect(subject).to match_array(candidates.values_at(2, 1))
+ end
+ end
+ end
+
+ describe 'sorting' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:test_case, :order_by, :order_by_type, :direction, :expected_order) do
+ 'default params' | nil | nil | nil | [3, 2, 1, 0]
+ 'ascending order' | nil | nil | 'ASC' | [0, 1, 2, 3]
+ 'column is passed' | 'name' | 'column' | 'ASC' | [1, 3, 0, 2]
+ 'column is a metric' | 'auc' | 'metric' | nil | [2, 0, 1, 3]
+ 'invalid sort' | nil | nil | 'INVALID' | [3, 2, 1, 0]
+ 'invalid order by' | 'INVALID' | 'column' | 'desc' | [3, 2, 1, 0]
+ 'invalid order by metric' | nil | 'metric' | 'desc' | []
+ end
+ with_them do
+ let(:params) { { order_by: order_by, order_by_type: order_by_type, sort: direction } }
+
+ it { expect(subject).to eq(candidates.values_at(*expected_order)) }
+ end
+ end
+
+ context 'when name and sort by metric is passed' do
+ let(:params) { { order_by: 'auc', order_by_type: 'metric', sort: 'DESC', name: 'a' } }
+
+ it { expect(subject).to eq(candidates.values_at(2, 1)) }
+ end
+ end
+
+ private
+
+ def make_candidate_and_metric(name, auc_value, experiment)
+ create(:ml_candidates, name: name, experiment: experiment, user: nil).tap do |c|
+ create(:ml_candidate_metrics, name: 'auc', candidate_id: c.id, value: 10)
+ create(:ml_candidate_metrics, name: 'auc', candidate_id: c.id, value: auc_value)
+ end
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 9fecbfb71fc..297c6f84cef 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -14,11 +14,11 @@ RSpec.describe ProjectsFinder do
end
let_it_be(:internal_project) do
- create(:project, :internal, group: group, name: 'B', path: 'B')
+ create(:project, :internal, :merge_requests_disabled, group: group, name: 'B', path: 'B')
end
let_it_be(:public_project) do
- create(:project, :public, group: group, name: 'C', path: 'C')
+ create(:project, :public, :merge_requests_enabled, :issues_disabled, group: group, name: 'C', path: 'C')
end
let_it_be(:shared_project) do
@@ -399,14 +399,18 @@ RSpec.describe ProjectsFinder do
let(:params) { { language: ruby.id } }
it { is_expected.to match_array([internal_project]) }
+ end
- context 'when project_language_search feature flag disabled' do
- before do
- stub_feature_flags(project_language_search: false)
- end
+ describe 'when with_issues_enabled is true' do
+ let(:params) { { with_issues_enabled: true } }
- it { is_expected.to match_array([internal_project, public_project]) }
- end
+ it { is_expected.to match_array([internal_project]) }
+ end
+
+ describe 'when with_merge_requests_enabled is true' do
+ let(:params) { { with_merge_requests_enabled: true } }
+
+ it { is_expected.to match_array([public_project]) }
end
describe 'sorting' do
diff --git a/spec/finders/protected_branches_finder_spec.rb b/spec/finders/protected_branches_finder_spec.rb
index 487d1be697a..5926891ac9d 100644
--- a/spec/finders/protected_branches_finder_spec.rb
+++ b/spec/finders/protected_branches_finder_spec.rb
@@ -3,35 +3,57 @@
require 'spec_helper'
RSpec.describe ProtectedBranchesFinder do
- let(:project) { create(:project) }
- let!(:protected_branch) { create(:protected_branch, project: project) }
- let!(:another_protected_branch) { create(:protected_branch, project: project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+
+ let!(:project_protected_branch) { create(:protected_branch, project: project) }
+ let!(:another_project_protected_branch) { create(:protected_branch, project: project) }
+ let!(:group_protected_branch) { create(:protected_branch, project: nil, group: group) }
+ let!(:another_group_protected_branch) { create(:protected_branch, project: nil, group: group) }
let!(:other_protected_branch) { create(:protected_branch) }
+
let(:params) { {} }
+ subject { described_class.new(entity, params).execute }
+
describe '#execute' do
- subject { described_class.new(project, params).execute }
+ shared_examples 'execute by entity' do
+ it 'returns all protected branches of project by default' do
+ expect(subject).to match_array(expected_branches)
+ end
- it 'returns all protected branches of project by default' do
- expect(subject).to match_array([protected_branch, another_protected_branch])
- end
+ context 'when search param is present' do
+ let(:params) { { search: group_protected_branch.name } }
- context 'when search param is present' do
- let(:params) { { search: protected_branch.name } }
+ it 'filters by search param' do
+ expect(subject).to eq([group_protected_branch])
+ end
+ end
+
+ context 'when there are more protected branches than the limit' do
+ before do
+ stub_const("#{described_class}::LIMIT", 1)
+ end
- it 'filters by search param' do
- expect(subject).to eq([protected_branch])
+ it 'returns limited protected branches of project' do
+ expect(subject.count).to eq(1)
+ end
end
end
- context 'when there are more protected branches than the limit' do
- before do
- stub_const("#{described_class}::LIMIT", 1)
+ it_behaves_like 'execute by entity' do
+ let(:entity) { project }
+ let(:expected_branches) do
+ [
+ project_protected_branch, another_project_protected_branch,
+ group_protected_branch, another_group_protected_branch
+ ]
end
+ end
- it 'returns limited protected branches of project' do
- expect(subject.count).to eq(1)
- end
+ it_behaves_like 'execute by entity' do
+ let(:entity) { group }
+ let(:expected_branches) { [group_protected_branch, another_group_protected_branch] }
end
end
end
diff --git a/spec/finders/releases/group_releases_finder_spec.rb b/spec/finders/releases/group_releases_finder_spec.rb
index 5eac6f4fbdc..c47477eb3d5 100644
--- a/spec/finders/releases/group_releases_finder_spec.rb
+++ b/spec/finders/releases/group_releases_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::GroupReleasesFinder do
+RSpec.describe Releases::GroupReleasesFinder, feature_category: :subgroups do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index bcead6b0170..c0203dd6132 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 WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert]
+ %w[Epic Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert Namespace Project]
else
- %w[Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert]
+ %w[Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert Namespace Project]
end
expect(described_class.todo_types).to contain_exactly(*expected_result)
diff --git a/spec/fixtures/api/schemas/entities/codequality_degradation.json b/spec/fixtures/api/schemas/entities/codequality_degradation.json
index 863b9f0c77e..ac772873daf 100644
--- a/spec/fixtures/api/schemas/entities/codequality_degradation.json
+++ b/spec/fixtures/api/schemas/entities/codequality_degradation.json
@@ -21,7 +21,10 @@
},
"web_url": {
"type": "string"
+ },
+ "engine_name": {
+ "type": "string"
}
},
"additionalProperties": false
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 1f3de0e0ff5..f66f5eb35b5 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -9,6 +9,7 @@
"version",
"packageType",
"project",
+ "publicPackage",
"tags",
"pipelines",
"versions",
@@ -31,13 +32,20 @@
"type": "string"
},
"version": {
- "type": ["string", "null"]
+ "type": [
+ "string",
+ "null"
+ ]
},
"canDestroy": {
- "type": ["boolean"]
+ "type": [
+ "boolean"
+ ]
},
"packageType": {
- "type": ["string"],
+ "type": [
+ "string"
+ ],
"enum": [
"MAVEN",
"NPM",
@@ -56,66 +64,126 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "count": { "type": "integer" },
- "pageInfo": { "type": "object" },
- "edges": { "type": "array" },
- "nodes": { "type": "array" }
+ "count": {
+ "type": "integer"
+ },
+ "pageInfo": {
+ "type": "object"
+ },
+ "edges": {
+ "type": "array"
+ },
+ "nodes": {
+ "type": "array"
+ }
}
},
"project": {
"type": "object"
},
+ "publicPackage": {
+ "type": "boolean"
+ },
"pipelines": {
"type": "object",
"additionalProperties": false,
"properties": {
- "pageInfo": { "type": "object" },
- "count": { "type": "integer" },
- "edges": { "type": "array" },
- "nodes": { "type": "array" }
+ "pageInfo": {
+ "type": "object"
+ },
+ "count": {
+ "type": "integer"
+ },
+ "edges": {
+ "type": "array"
+ },
+ "nodes": {
+ "type": "array"
+ }
}
},
"versions": {
"type": "object",
"additionalProperties": false,
"properties": {
- "count": { "type": "integer" },
- "pageInfo": { "type": "object" },
- "edges": { "type": "array" },
- "nodes": { "type": "array" }
+ "count": {
+ "type": "integer"
+ },
+ "pageInfo": {
+ "type": "object"
+ },
+ "edges": {
+ "type": "array"
+ },
+ "nodes": {
+ "type": "array"
+ }
}
},
"metadata": {
"anyOf": [
- { "$ref": "./package_composer_metadata.json" },
- { "$ref": "./package_conan_metadata.json" },
- { "$ref": "./package_maven_metadata.json" },
- { "$ref": "./package_nuget_metadata.json" },
- { "$ref": "./package_pypi_metadata.json" },
- { "type": "null" }
+ {
+ "$ref": "./package_composer_metadata.json"
+ },
+ {
+ "$ref": "./package_conan_metadata.json"
+ },
+ {
+ "$ref": "./package_maven_metadata.json"
+ },
+ {
+ "$ref": "./package_nuget_metadata.json"
+ },
+ {
+ "$ref": "./package_pypi_metadata.json"
+ },
+ {
+ "type": "null"
+ }
]
},
"packageFiles": {
"type": "object",
"additionalProperties": false,
"properties": {
- "count": { "type": "integer" },
- "pageInfo": { "type": "object" },
- "edges": { "type": "array" },
- "nodes": { "type": "array" }
+ "count": {
+ "type": "integer"
+ },
+ "pageInfo": {
+ "type": "object"
+ },
+ "edges": {
+ "type": "array"
+ },
+ "nodes": {
+ "type": "array"
+ }
}
},
"status": {
- "type": ["string"],
- "enum": ["DEFAULT", "HIDDEN", "PROCESSING", "ERROR"]
+ "type": [
+ "string"
+ ],
+ "enum": [
+ "DEFAULT",
+ "HIDDEN",
+ "PROCESSING",
+ "ERROR"
+ ]
},
"dependencyLinks": {
"type": "object",
"additionalProperties": false,
"properties": {
- "pageInfo": { "type": "object" },
- "edges": { "type": "array" },
- "count": { "type": "integer" },
+ "pageInfo": {
+ "type": "object"
+ },
+ "edges": {
+ "type": "array"
+ },
+ "count": {
+ "type": "integer"
+ },
"nodes": {
"type": "array",
"items": {
@@ -143,8 +211,12 @@
},
"metadata": {
"anyOf": [
- { "$ref": "./package_nuget_dependency_link_metadata.json" },
- { "type": "null" }
+ {
+ "$ref": "./package_nuget_dependency_link_metadata.json"
+ },
+ {
+ "type": "null"
+ }
]
}
}
@@ -177,14 +249,20 @@
"type": "string"
},
"lastDownloadedAt": {
- "type": ["string", "null"]
+ "type": [
+ "string",
+ "null"
+ ]
},
"_links": {
"type": "object",
"additionalProperties": false,
"properties": {
"webPath": {
- "type": ["string", "null"]
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
index f6b12d3a1c0..6265fbcff69 100644
--- a/spec/fixtures/api/schemas/public_api/v4/job.json
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -11,6 +11,7 @@
"created_at",
"started_at",
"finished_at",
+ "erased_at",
"duration",
"queued_duration",
"user",
@@ -35,6 +36,7 @@
"created_at": { "type": "string" },
"started_at": { "type": ["null", "string"] },
"finished_at": { "type": ["null", "string"] },
+ "erased_at": { "type": ["null", "string"] },
"duration": { "type": ["null", "number"] },
"queued_duration": { "type": ["null", "number"] },
"user": { "$ref": "user/basic.json" },
diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md
index 18500d94c7a..c60daf49f3d 100644
--- a/spec/fixtures/blockquote_fence_after.md
+++ b/spec/fixtures/blockquote_fence_after.md
@@ -130,6 +130,35 @@ Double `>>>` inside HTML inside blockquote:
> Quote
+Blockquote inside an unordered list
+
+- Item one
+
+
+ > Foo and
+ > bar
+
+
+ - Sub item
+
+
+ > Foo
+
+
+Blockquote inside an ordered list
+
+1. Item one
+
+
+ > Bar
+
+
+ 1. Sub item
+
+
+ > Foo
+
+
Requires a leading blank line
>>>
Not a quote
diff --git a/spec/fixtures/blockquote_fence_before.md b/spec/fixtures/blockquote_fence_before.md
index 895bff73404..1f4b8196def 100644
--- a/spec/fixtures/blockquote_fence_before.md
+++ b/spec/fixtures/blockquote_fence_before.md
@@ -130,6 +130,35 @@ Quote
Quote
>>>
+Blockquote inside an unordered list
+
+- Item one
+
+ >>>
+ Foo and
+ bar
+ >>>
+
+ - Sub item
+
+ >>>
+ Foo
+ >>>
+
+Blockquote inside an ordered list
+
+1. Item one
+
+ >>>
+ Bar
+ >>>
+
+ 1. Sub item
+
+ >>>
+ Foo
+ >>>
+
Requires a leading blank line
>>>
Not a quote
diff --git a/spec/fixtures/build_artifacts/dotenv_utf16_le.txt b/spec/fixtures/build_artifacts/dotenv_utf16_le.txt
new file mode 100644
index 00000000000..6ff398f70c5
--- /dev/null
+++ b/spec/fixtures/build_artifacts/dotenv_utf16_le.txt
Binary files differ
diff --git a/spec/fixtures/database.sql.gz b/spec/fixtures/database.sql.gz
new file mode 100644
index 00000000000..a98aa7c53f2
--- /dev/null
+++ b/spec/fixtures/database.sql.gz
Binary files differ
diff --git a/spec/fixtures/emails/html_only.eml b/spec/fixtures/emails/html_only.eml
new file mode 100644
index 00000000000..22a1a431771
--- /dev/null
+++ b/spec/fixtures/emails/html_only.eml
@@ -0,0 +1,45 @@
+Delivered-To: reply@discourse.org
+Return-Path: <walter.white@googlemail.com>
+MIME-Version: 1.0
+In-Reply-To: <topic/22638/86406@meta.discourse.org>
+References: <topic/22638@meta.discourse.org>
+ <topic/22638/86406@meta.discourse.org>
+Date: Fri, 28 Nov 2014 12:36:49 -0800
+Subject: Re: [Discourse Meta] [Lounge] Testing default email replies
+From: Walter White <walter.white@googlemail.com>
+To: Discourse Meta <reply@discourse.org>
+Content-Type: multipart/related; boundary=001a11c2e04e6544f30508f138ba
+
+--001a11c2e04e6544f30508f138ba
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<div dir=3D"ltr"><div>### This is a reply from standard GMail in Google Chr=
+ome.</div><div><br></div><div>The quick brown fox jumps over the lazy dog. =
+The quick brown fox jumps over the lazy dog. The quick brown fox jumps over=
+ the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown=
+ fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. =
+The quick brown fox jumps over the lazy dog. The quick brown fox jumps over=
+ the lazy dog.=C2=A0</div><div><br></div><div>Here&#39;s some **bold** text=
+, <strong>strong</strong> text and <em>italic</em> in Markdown.</div><div><=
+br></div><div>Here&#39;s a link <a href=3D"http://example.com">http://examp=
+le.com</a></div></div><div class=3D"gmail_extra"><br>Here&#39;s an img <i=
+mg class="header__logoSize_110px" src="http://img.png" hspace="0" vspac=
+e="0" border="0" style="display:block; width:138px; max-width:138px;" width=
+="138" alt="Miro"><details><summary>One</summary> Some details</details>
+<details><summary>Two</summary> Some details</details></div>
+
+<table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" bor=
+der=3D"0">
+ <tbody>
+ <tr>
+ <td style=3D"padding-top:5px" colspan=3D"2">
+ <p style=3D"margin-top:0;border:0">Test reply.</p>
+ <p style=3D"margin-top:0;border:0">First paragraph.</p>
+ <p style=3D"margin-top:0;border:0">Second paragraph.</p>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+--001a11c2e04e6544f30508f138ba--
diff --git a/spec/fixtures/emails/html_table_and_blockquote.eml b/spec/fixtures/emails/html_table_and_blockquote.eml
new file mode 100644
index 00000000000..554d17def50
--- /dev/null
+++ b/spec/fixtures/emails/html_table_and_blockquote.eml
@@ -0,0 +1,41 @@
+MIME-Version: 1.0
+Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT)
+X-Originating-IP: [117.207.85.84]
+In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+References: <topic/35@discourse.techapj.com>
+ <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+Date: Wed, 8 Oct 2014 10:47:17 +0530
+Delivered-To: arpit@techapj.com
+Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com>
+Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse!
+From: Arpit Jalan <arpit@techapj.com>
+To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>
+Content-Type: multipart/related; boundary=001a114119d8f4e46e0504e26d5b
+
+--001a114119d8f4e46e0504e26d5b
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<table>
+ <tr>
+ <th>Company</th>
+ <th>Contact</th>
+ <th>Country</th>
+ </tr>
+ <tr>
+ <td>Alfreds Futterkiste</td>
+ <td>Maria Anders</td>
+ <td>Germany</td>
+ </tr>
+ <tr>
+ <td>Centro comercial Moctezuma</td>
+ <td>Francisco Chang</td>
+ <td>Mexico</td>
+ </tr>
+</table>
+
+<blockquote cite="https://www.huxley.net/bnw/four.html">
+ <p>Words can be like X-rays, if you use them properly—they’ll go through anything. You read and you’re pierced.</p>
+</blockquote>
+
+--001a114119d8f4e46e0504e26d5b--
diff --git a/spec/fixtures/lib/gitlab/email/basic.html b/spec/fixtures/lib/gitlab/email/basic.html
new file mode 100644
index 00000000000..807b23c46e3
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/email/basic.html
@@ -0,0 +1,72 @@
+<html>
+ <title>Ignored Title</title>
+ <body>
+ <h1>Hello, World!</h1>
+
+ This is some e-mail content.
+ Even though it has whitespace and newlines, the e-mail converter
+ will handle it correctly.
+
+ <p><em>Even</em> mismatched tags.</p>
+
+ <div>A div</div>
+ <div>Another div</div>
+ <div>A div<div><strong>within</strong> a div</div></div>
+
+ <p>Another line<br />Yet another line</p>
+
+ <a href="http://foo.com">A link</a>
+
+ <p><details><summary>One</summary>Some details</details></p>
+
+ <p><details><summary>Two</summary>Some details</details></p>
+
+ <img class="header__logoSize_110px" src="http://img.png" hspace="0" vspace="0" border="0"
+ style="display:block; width:138px; max-width:138px;" width="138" alt="Miro">
+
+ <table>
+ <thead>
+ <tr>
+ <th>Col A</th>
+ <th>Col B</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ Data A1
+ </td>
+ <td>
+ Data B1
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Data A2
+ </td>
+ <td>
+ Data B2
+ </td>
+ </tr>
+ <tr>
+ <td>
+ Data A3
+ </td>
+ <td>
+ Data B4
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td>
+ Total A
+ </td>
+ <td>
+ Total B
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </body>
+</html>
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index 88439965cf3..0bca7b0f494 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -18,7 +18,9 @@
"template": false,
"description": "",
"type": "ProjectLabel",
- "priorities": []
+ "priorities": [
+
+ ]
},
{
"id": 3,
@@ -202,7 +204,9 @@
"author": {
"name": "User 4"
},
- "events": [],
+ "events": [
+
+ ],
"award_emoji": [
{
"id": 1,
@@ -235,7 +239,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 353,
@@ -257,7 +263,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 354,
@@ -279,7 +287,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 355,
@@ -301,7 +311,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 356,
@@ -323,7 +335,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 357,
@@ -345,7 +359,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 358,
@@ -367,7 +383,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"resource_label_events": [
@@ -448,7 +466,9 @@
"confidential": false,
"due_date": null,
"moved_to_id": null,
- "issue_assignees": [],
+ "issue_assignees": [
+
+ ],
"milestone": {
"id": 1,
"title": "test milestone",
@@ -493,7 +513,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 360,
@@ -515,7 +537,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 361,
@@ -537,7 +561,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 362,
@@ -559,7 +585,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 363,
@@ -581,7 +609,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 364,
@@ -603,7 +633,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 365,
@@ -625,7 +657,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 366,
@@ -647,7 +681,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -709,7 +745,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 368,
@@ -731,7 +769,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 369,
@@ -753,7 +793,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 370,
@@ -775,7 +817,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 371,
@@ -797,7 +841,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 372,
@@ -819,7 +865,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 373,
@@ -841,7 +889,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 374,
@@ -863,7 +913,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -904,7 +956,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 376,
@@ -926,7 +980,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 377,
@@ -948,7 +1004,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 378,
@@ -970,7 +1028,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 379,
@@ -992,7 +1052,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 380,
@@ -1014,7 +1076,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 381,
@@ -1036,7 +1100,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 382,
@@ -1058,7 +1124,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -1099,7 +1167,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 384,
@@ -1121,7 +1191,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 385,
@@ -1143,7 +1215,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 386,
@@ -1165,7 +1239,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 387,
@@ -1187,7 +1263,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 388,
@@ -1209,7 +1287,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 389,
@@ -1231,7 +1311,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 390,
@@ -1253,7 +1335,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -1294,7 +1378,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 392,
@@ -1316,7 +1402,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 393,
@@ -1338,7 +1426,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 394,
@@ -1360,7 +1450,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 395,
@@ -1382,7 +1474,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 396,
@@ -1404,7 +1498,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 397,
@@ -1426,7 +1522,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 398,
@@ -1448,7 +1546,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -1489,7 +1589,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 400,
@@ -1511,7 +1613,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 401,
@@ -1533,7 +1637,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 402,
@@ -1555,7 +1661,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 403,
@@ -1577,7 +1685,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 404,
@@ -1599,7 +1709,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 405,
@@ -1621,7 +1733,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 406,
@@ -1643,7 +1757,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -1684,7 +1800,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 408,
@@ -1706,7 +1824,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 409,
@@ -1728,7 +1848,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 410,
@@ -1750,7 +1872,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 411,
@@ -1772,7 +1896,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 412,
@@ -1794,7 +1920,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 413,
@@ -1816,7 +1944,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 414,
@@ -1838,7 +1968,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -1879,7 +2011,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 416,
@@ -1901,7 +2035,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 417,
@@ -1923,7 +2059,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 418,
@@ -1945,7 +2083,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 419,
@@ -1967,7 +2107,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 420,
@@ -1989,7 +2131,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 421,
@@ -2011,7 +2155,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 422,
@@ -2033,7 +2179,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
},
@@ -2084,7 +2232,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 424,
@@ -2106,7 +2256,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 425,
@@ -2128,7 +2280,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 426,
@@ -2150,7 +2304,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 427,
@@ -2172,7 +2328,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 428,
@@ -2194,7 +2352,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 429,
@@ -2216,7 +2376,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 430,
@@ -2238,7 +2400,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
]
}
@@ -2378,7 +2542,9 @@
"author": {
"name": "Random name"
},
- "events": [],
+ "events": [
+
+ ],
"award_emoji": [
{
"id": 12,
@@ -2652,7 +2818,9 @@
"author": {
"name": "User 4"
},
- "award_emoji": [],
+ "award_emoji": [
+
+ ],
"system_note_metadata": {
"id": 4789,
"commit_count": 3,
@@ -2660,8 +2828,12 @@
"created_at": "2020-03-28T12:47:33.461Z",
"updated_at": "2020-03-28T12:47:33.461Z"
},
- "events": [],
- "suggestions": []
+ "events": [
+
+ ],
+ "suggestions": [
+
+ ]
},
{
"id": 670,
@@ -2691,7 +2863,9 @@
"author": {
"name": "User 4"
},
- "award_emoji": [],
+ "award_emoji": [
+
+ ],
"system_note_metadata": {
"id": 4790,
"commit_count": null,
@@ -2699,8 +2873,12 @@
"created_at": "2020-03-28T12:48:36.951Z",
"updated_at": "2020-03-28T12:48:36.951Z"
},
- "events": [],
- "suggestions": []
+ "events": [
+
+ ],
+ "suggestions": [
+
+ ]
},
{
"id": 671,
@@ -2724,7 +2902,9 @@
"author": {
"name": "User 4"
},
- "events": [],
+ "events": [
+
+ ],
"award_emoji": [
{
"id": 1,
@@ -2757,7 +2937,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 673,
@@ -2779,7 +2961,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 674,
@@ -2801,7 +2985,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [],
+ "events": [
+
+ ],
"suggestions": [
{
"id": 1,
@@ -2837,7 +3023,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 676,
@@ -2859,7 +3047,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 677,
@@ -2881,7 +3071,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 678,
@@ -2903,7 +3095,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"resource_label_events": [
@@ -3332,7 +3526,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 680,
@@ -3354,7 +3550,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 681,
@@ -3376,7 +3574,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 682,
@@ -3398,7 +3598,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 683,
@@ -3420,7 +3622,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 684,
@@ -3442,7 +3646,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 685,
@@ -3464,7 +3670,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 686,
@@ -3486,7 +3694,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -3553,9 +3763,15 @@
"author_id": 1
}
],
- "merge_request_assignees": [],
- "merge_request_reviewers": [],
- "approvals": []
+ "merge_request_assignees": [
+
+ ],
+ "merge_request_reviewers": [
+
+ ],
+ "approvals": [
+
+ ]
},
{
"id": 15,
@@ -3602,7 +3818,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 778,
@@ -3624,7 +3842,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 779,
@@ -3646,7 +3866,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 780,
@@ -3668,7 +3890,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 781,
@@ -3690,7 +3914,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 782,
@@ -3712,7 +3938,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 783,
@@ -3734,7 +3962,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 784,
@@ -3756,7 +3986,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -3869,7 +4101,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 786,
@@ -3891,7 +4125,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 787,
@@ -3913,7 +4149,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 788,
@@ -3935,7 +4173,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 789,
@@ -3957,7 +4197,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 790,
@@ -3979,7 +4221,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 791,
@@ -4001,7 +4245,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 792,
@@ -4023,7 +4269,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -4656,7 +4904,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 795,
@@ -4678,7 +4928,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 796,
@@ -4700,7 +4952,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 797,
@@ -4722,7 +4976,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 798,
@@ -4744,7 +5000,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 799,
@@ -4766,7 +5024,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 800,
@@ -4788,7 +5048,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -5203,7 +5465,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 802,
@@ -5225,7 +5489,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 803,
@@ -5247,7 +5513,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 804,
@@ -5269,7 +5537,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 805,
@@ -5291,7 +5561,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 806,
@@ -5313,7 +5585,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 807,
@@ -5335,7 +5609,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 808,
@@ -5357,7 +5633,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -5727,7 +6005,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 810,
@@ -5749,7 +6029,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 811,
@@ -5771,7 +6053,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 812,
@@ -5793,7 +6077,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 813,
@@ -5815,7 +6101,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 814,
@@ -5837,7 +6125,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 815,
@@ -5859,7 +6149,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 816,
@@ -5881,14 +6173,20 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
"id": 11,
"state": "empty",
- "merge_request_diff_commits": [],
- "merge_request_diff_files": [],
+ "merge_request_diff_commits": [
+
+ ],
+ "merge_request_diff_files": [
+
+ ],
"merge_request_id": 11,
"created_at": "2016-06-14T15:02:23.772Z",
"updated_at": "2016-06-14T15:02:23.833Z",
@@ -5963,7 +6261,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 818,
@@ -5985,7 +6285,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 819,
@@ -6007,7 +6309,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 820,
@@ -6029,7 +6333,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 821,
@@ -6051,7 +6357,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 822,
@@ -6073,7 +6381,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 823,
@@ -6095,7 +6405,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 824,
@@ -6117,7 +6429,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -6716,7 +7030,9 @@
"author": {
"name": "User 4"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 826,
@@ -6738,7 +7054,9 @@
"author": {
"name": "User 3"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 827,
@@ -6760,7 +7078,9 @@
"author": {
"name": "User 0"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 828,
@@ -6782,7 +7102,9 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 829,
@@ -6804,7 +7126,9 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 830,
@@ -6826,7 +7150,9 @@
"author": {
"name": "Burdette Bernier"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 831,
@@ -6848,7 +7174,9 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": []
+ "events": [
+
+ ]
},
{
"id": 832,
@@ -6870,7 +7198,9 @@
"author": {
"name": "Administrator"
},
- "events": []
+ "events": [
+
+ ]
}
],
"merge_request_diff": {
@@ -7474,7 +7804,9 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "stages": []
+ "stages": [
+
+ ]
},
{
"id": 20,
@@ -7492,7 +7824,9 @@
"started_at": null,
"finished_at": null,
"duration": null,
- "stages": [],
+ "stages": [
+
+ ],
"source": "external_pull_request_event",
"external_pull_request": {
"id": 3,
@@ -7535,8 +7869,12 @@
"keep_n": 100,
"enabled": false
},
- "deploy_keys": [],
- "hooks": [],
+ "deploy_keys": [
+
+ ],
+ "hooks": [
+
+ ],
"protected_branches": [
{
"id": 1,
@@ -7791,7 +8129,9 @@
"description": null,
"group_id": null,
"type": "ProjectLabel",
- "priorities": []
+ "priorities": [
+
+ ]
}
},
{
@@ -7847,4 +8187,4 @@
"commit_committer_check": true,
"regexp_uses_re2": true
}
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json
index c40befcf8ce..0d9c217afd1 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json
@@ -7,7 +7,7 @@
"properties": {
"type": { "enum": ["custom"] },
"label": { "type": "string" },
- "options": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json" }
+ "options": { "$ref": "custom_variable_options.json" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json
index de72b947eed..bb78294e43e 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_options.json
@@ -5,7 +5,7 @@
"properties": {
"values": {
"type": "array",
- "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_values.json" }
+ "items": { "$ref": "custom_variable_values.json" }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
index 40453c61a65..f38f74ae13f 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
@@ -11,10 +11,10 @@
"priority": { "type": "number" },
"panel_groups": {
"type": "array",
- "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" }
+ "items": { "$ref": "panel_groups.json" }
},
- "templating": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json" },
- "links": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/links.json" }
+ "templating": { "$ref": "templating.json" },
+ "links": { "$ref": "links.json" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json
index b47b81fc103..a5228bc0888 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json
@@ -6,7 +6,7 @@
"panel_groups": {
"type": "array",
"items": {
- "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json"
+ "$ref": "embedded_panel_groups.json"
}
}
},
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json
index 063016c22fd..b1c34ba1b86 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/embedded_panel_groups.json
@@ -5,7 +5,7 @@
"properties": {
"panels": {
"type": "array",
- "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json" }
+ "items": { "$ref": "panels.json" }
}
},
"additionalProperties": false
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 145cc476d64..742708e60bd 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
@@ -15,8 +15,8 @@
"type": "string"
},
"options": {
- "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_options.json"
+ "$ref": "metric_label_values_variable_options.json"
}
},
"additionalProperties": false
-} \ No newline at end of file
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
index 392aa0e4480..a5a4428f2f3 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
@@ -9,7 +9,7 @@
"group": { "type": "string" },
"panels": {
"type": "array",
- "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json" }
+ "items": { "$ref": "panels.json" }
},
"has_custom_metrics": { "type": "boolean" }
},
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
index 3224e7cfe3f..78369a7a055 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -11,11 +11,11 @@
"id": { "type": "string" },
"type": { "type": "string" },
"y_label": { "type": "string" },
- "y_axis": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/axis.json" },
+ "y_axis": { "$ref": "axis.json" },
"max_value": { "type": "number" },
"metrics": {
"type": "array",
- "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json" }
+ "items": { "$ref": "metrics.json" }
}
},
"additionalProperties": false
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json
index 439f7b6b044..c339edec128 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/templating.json
@@ -3,7 +3,7 @@
"type": "object",
"required": ["variables"],
"properties": {
- "variables": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json" }
+ "variables": { "$ref": "variables.json" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json
index c4382326854..37ff4fdba5f 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json
@@ -7,7 +7,7 @@
"properties": {
"type": { "enum": ["text"] },
"label": { "type": "string" },
- "options": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_options.json" }
+ "options": { "$ref": "text_variable_options.json" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json
index 1cf5ae2eaa4..73841d5bd82 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/variables.json
@@ -4,14 +4,14 @@
"patternProperties": {
"^[a-zA-Z0-9_]*$": {
"anyOf": [
- { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/text_variable_full_syntax.json" },
+ { "$ref": "text_variable_full_syntax.json" },
{ "type": "string" },
{
"type": "array",
"items": { "type": "string" }
},
- { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/custom_variable_full_syntax.json" },
- { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metric_label_values_variable_full_syntax.json" }
+ { "$ref": "custom_variable_full_syntax.json" },
+ { "$ref": "metric_label_values_variable_full_syntax.json" }
]
}
},
diff --git a/spec/fixtures/mail_room/encrypted_secrets/incoming_email.yaml.enc b/spec/fixtures/mail_room/encrypted_secrets/incoming_email.yaml.enc
new file mode 100644
index 00000000000..c11722e6847
--- /dev/null
+++ b/spec/fixtures/mail_room/encrypted_secrets/incoming_email.yaml.enc
@@ -0,0 +1 @@
+v7dkZ/3zpOBsA6XhOq99ai5S4czZlkdLUmNcH8AYOw56b7PgEMmtLHu39Fcyd4ZvERJNMIqyLuCwooqvJKfUeFRi9HpY0Q==--GldddWssSV3R/Ood--1XlQ63E4XCVqcfZVAd+NGQ== \ No newline at end of file
diff --git a/spec/fixtures/mail_room/encrypted_secrets/service_desk_email.yaml.enc b/spec/fixtures/mail_room/encrypted_secrets/service_desk_email.yaml.enc
new file mode 100644
index 00000000000..bad1f2bbce1
--- /dev/null
+++ b/spec/fixtures/mail_room/encrypted_secrets/service_desk_email.yaml.enc
@@ -0,0 +1 @@
+nWE1RNp/i+RzboK4SqbQBI52cFP14G3hduqIOdus2Ffgul8n0vL/bKHMHaiCttq2hzGnzw5zcMGUWxJu3RkAhR0jnLAgbpBrYsxKTH72cqkJfw==--5FtWCIPwB8DldWkY--FLjDkVAsCzDM9VY1x7yifg== \ No newline at end of file
diff --git a/spec/fixtures/mail_room/secrets.yml.erb b/spec/fixtures/mail_room/secrets.yml.erb
new file mode 100644
index 00000000000..fce7ff3180e
--- /dev/null
+++ b/spec/fixtures/mail_room/secrets.yml.erb
@@ -0,0 +1,11 @@
+---
+production:
+ an_unread_key: 'this key will not be in the secrets'
+shared:
+ a_shared_key: 'this key is shared'
+ an_overriden_shared_key: 'this key is overwritten by merge'
+test:
+ an_environment_specific_key: 'test environment value'
+ an_overriden_shared_key: 'the merge overwrote this key'
+ erb_env_key: <%= ENV['KEY'] %>
+ encrypted_settings_key_base: <%= '0123456789abcdef' * 8 %>
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 38b2a8381bb..979e96e6e8e 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -400,6 +400,8 @@ Bob -> Sara : Hello
]
```
-### Image Attributes
+### Image/Video Attributes
![Sized Image](app/assets/images/touch-icon-ipad.png){width=75% height=100}
+
+![Sized Video](/assets/videos/gitlab-demo.mp4){width=75% height=100}
diff --git a/spec/fixtures/structure.sql b/spec/fixtures/structure.sql
new file mode 100644
index 00000000000..49061dfa8ea
--- /dev/null
+++ b/spec/fixtures/structure.sql
@@ -0,0 +1,20 @@
+CREATE INDEX missing_index ON events USING btree (created_at, author_id);
+
+CREATE UNIQUE INDEX wrong_index ON table_name (column_name, column_name_2);
+
+CREATE UNIQUE INDEX "index" ON achievements USING btree (namespace_id, lower(name));
+
+CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20));
+
+CREATE UNIQUE INDEX index_on_deploy_keys_id_and_type_and_public ON keys USING btree (id, type) WHERE (public = true);
+
+CREATE INDEX index_users_on_public_email_excluding_null_and_empty ON users USING btree (public_email) WHERE (((public_email)::text <> ''::text) AND (public_email IS NOT NULL));
+
+ALTER TABLE ONLY bulk_import_configurations
+ ADD CONSTRAINT fk_rails_536b96bff1 FOREIGN KEY (bulk_import_id) REFERENCES bulk_imports(id) ON DELETE CASCADE;
+
+CREATE TABLE ci_project_mirrors (
+ id bigint NOT NULL,
+ project_id integer NOT NULL,
+ namespace_id integer NOT NULL
+);
diff --git a/spec/fixtures/svg_without_attr.svg b/spec/fixtures/svg_without_attr.svg
new file mode 100644
index 00000000000..0864c6009ef
--- /dev/null
+++ b/spec/fixtures/svg_without_attr.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg viewBox="0 0 50 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
+ <title>Slice 1</title>
+ <desc>Created with Sketch.</desc>
+ <script>alert('FAIL')</script>
+ <defs></defs>
+ <path d="m49.014 19-.067-.18-6.784-17.696a1.792 1.792 0 0 0-3.389.182l-4.579 14.02H15.651l-4.58-14.02a1.795 1.795 0 0 0-3.388-.182l-6.78 17.7-.071.175A12.595 12.595 0 0 0 5.01 33.556l.026.02.057.044 10.32 7.734 5.12 3.87 3.11 2.351a2.102 2.102 0 0 0 2.535 0l3.11-2.352 5.12-3.869 10.394-7.779.029-.022a12.595 12.595 0 0 0 4.182-14.554Z"
+ fill="#E24329"/>
+ <path d="m49.014 19-.067-.18a22.88 22.88 0 0 0-9.12 4.103L24.931 34.187l9.485 7.167 10.393-7.779.03-.022a12.595 12.595 0 0 0 4.175-14.554Z"
+ fill="#FC6D26"/>
+ <path d="m15.414 41.354 5.12 3.87 3.11 2.351a2.102 2.102 0 0 0 2.535 0l3.11-2.352 5.12-3.869-9.484-7.167-9.51 7.167Z"
+ fill="#FCA326"/>
+ <path d="M10.019 22.923a22.86 22.86 0 0 0-9.117-4.1L.832 19A12.595 12.595 0 0 0 5.01 33.556l.026.02.057.044 10.32 7.734 9.491-7.167L10.02 22.923Z"
+ fill="#FC6D26"/>
+</svg>
+
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index 83ed0a869dc..d01affdaeac 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import initMRPage from '~/mr_notes';
import { getDiffFileMock } from '../diffs/mock_data/diff_file';
import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
@@ -37,7 +38,7 @@ export default function initVueMRPage() {
mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
- mock.onGet(diffsAppEndpoint).reply(200, {
+ mock.onGet(diffsAppEndpoint).reply(HTTP_STATUS_OK, {
branch_name: 'foo',
diff_files: [getDiffFileMock()],
});
diff --git a/spec/frontend/__helpers__/test_apollo_link.js b/spec/frontend/__helpers__/test_apollo_link.js
index eab0c2de212..d9e7f5fc348 100644
--- a/spec/frontend/__helpers__/test_apollo_link.js
+++ b/spec/frontend/__helpers__/test_apollo_link.js
@@ -18,7 +18,7 @@ const FOO_QUERY = gql`
*
* @returns Promise resolving to the resulting operation after running the subjectLink
*/
-export const testApolloLink = (subjectLink, options = {}) =>
+export const testApolloLink = (subjectLink, options = {}, query = FOO_QUERY) =>
new Promise((resolve) => {
const { context = {} } = options;
@@ -38,6 +38,6 @@ export const testApolloLink = (subjectLink, options = {}) =>
// Trigger a query so the ApolloLink chain will be executed.
client.query({
context,
- query: FOO_QUERY,
+ query,
});
});
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index 182aea9c1c5..4bd21ff150a 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import testActionFn from './vuex_action_helper';
const testActionFnWithOptionsArg = (...args) => {
@@ -101,7 +102,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
};
it('returns original data of successful promise while checking actions/mutations', async () => {
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_OK, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
@@ -110,7 +111,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
it('returns original error of rejected promise while checking actions/mutations', async () => {
- mock.onGet(TEST_HOST).replyOnce(500, '');
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
@@ -137,7 +138,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
};
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_OK, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
index 6efd9fb1dd0..ec20088c443 100644
--- a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -13,18 +13,18 @@ describe('AbuseCategorySelector', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = '1';
+ const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
wrapper = shallowMountExtended(AbuseCategorySelector, {
propsData: {
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
...props,
},
provide: {
reportAbusePath: ACTION_PATH,
- reportedUserId: USER_ID,
- reportedFromUrl: REPORTED_FROM_URL,
},
});
};
@@ -106,7 +106,7 @@ describe('AbuseCategorySelector', () => {
expect(findUserId().attributes()).toMatchObject({
type: 'hidden',
name: 'user_id',
- value: USER_ID,
+ value: USER_ID.toString(),
});
});
diff --git a/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js b/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js
new file mode 100644
index 00000000000..c0c87dd1383
--- /dev/null
+++ b/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js
@@ -0,0 +1,65 @@
+import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import LinksToSpamInput from '~/abuse_reports/components/links_to_spam_input.vue';
+
+describe('LinksToSpamInput', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(LinksToSpamInput, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
+ const findLinkInput = () => wrapper.findComponent(GlFormInput);
+ const findAddAnotherButton = () => wrapper.findComponent(GlButton);
+
+ describe('Form Input', () => {
+ it('renders only one input field initially', () => {
+ expect(findAllFormGroups()).toHaveLength(1);
+ });
+
+ it('is of type URL and has a name attribute', () => {
+ expect(findLinkInput().attributes()).toMatchObject({
+ type: 'url',
+ name: 'abuse_report[links_to_spam][]',
+ value: '',
+ });
+ });
+
+ describe('when add another link button is clicked', () => {
+ it('adds another input', async () => {
+ findAddAnotherButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findAllFormGroups()).toHaveLength(2);
+ });
+ });
+
+ describe('when previously added links are passed to the form as props', () => {
+ beforeEach(() => {
+ createComponent({ previousLinks: ['https://gitlab.com'] });
+ });
+
+ it('renders the input field with the value of the link pre-filled', () => {
+ expect(findAllFormGroups()).toHaveLength(1);
+
+ expect(findLinkInput().attributes()).toMatchObject({
+ type: 'url',
+ name: 'abuse_report[links_to_spam][]',
+ value: 'https://gitlab.com',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index 4b58a69c2b8..27c8d760a96 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/add_context_commits_modal/store/actions';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NO_CONTENT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AddContextCommitsModalStoreActions', () => {
const contextCommitEndpoint =
@@ -99,7 +100,7 @@ describe('AddContextCommitsModalStoreActions', () => {
describe('createContextCommits', () => {
it('calls API to create context commits', async () => {
- mock.onPost(contextCommitEndpoint).reply(200, {});
+ mock.onPost(contextCommitEndpoint).reply(HTTP_STATUS_OK, {});
await testAction(createContextCommits, { commits: [] }, {}, [], []);
@@ -116,7 +117,7 @@ describe('AddContextCommitsModalStoreActions', () => {
.onGet(
`/api/${gon.api_version}/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits`,
)
- .reply(200, [dummyCommit]);
+ .reply(HTTP_STATUS_OK, [dummyCommit]);
});
it('commits FETCH_CONTEXT_COMMITS', () => {
const contextCommit = { ...dummyCommit, isSelected: true };
@@ -156,7 +157,7 @@ describe('AddContextCommitsModalStoreActions', () => {
beforeEach(() => {
mock
.onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
- .reply(204);
+ .reply(HTTP_STATUS_NO_CONTENT);
});
it('calls API to remove context commits', () => {
return testAction(
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
index 020e1c1d7c1..d69bf4a22bf 100644
--- a/spec/frontend/admin/broadcast_messages/components/base_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -6,6 +6,7 @@ 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
@@ -70,7 +71,7 @@ describe('BroadcastMessagesBase', () => {
it('does not remove a deleted message if the request fails', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
- axiosMock.onDelete(delete_path).replyOnce(500);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
@@ -86,7 +87,7 @@ describe('BroadcastMessagesBase', () => {
it('removes a deleted message from visibleMessages on success', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
- axiosMock.onDelete(delete_path).replyOnce(200);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_OK);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
@@ -102,7 +103,7 @@ describe('BroadcastMessagesBase', () => {
const { id, delete_path } = messages[0];
createComponent({ messages, messagesCount: messages.length });
- axiosMock.onDelete(delete_path).replyOnce(200);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_OK);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index 190f0eb94a0..4c362a31068 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -8,6 +8,7 @@ import statisticsLabels from '~/admin/statistics_panel/constants';
import createStore from '~/admin/statistics_panel/store';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
Vue.use(Vuex);
@@ -25,7 +26,7 @@ describe('Admin statistics app', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(200);
+ axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(HTTP_STATUS_OK);
store = createStore();
});
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
index e7cdb5feb6a..20d5860a459 100644
--- a/spec/frontend/admin/statistics_panel/store/actions_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as actions from '~/admin/statistics_panel/store/actions';
import * as types from '~/admin/statistics_panel/store/mutation_types';
import getInitialState from '~/admin/statistics_panel/store/state';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
describe('Admin statistics panel actions', () => {
@@ -19,7 +20,7 @@ describe('Admin statistics panel actions', () => {
describe('fetchStatistics', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics);
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(HTTP_STATUS_OK, mockStatistics);
});
it('dispatches success with received data', () => {
@@ -43,7 +44,9 @@ describe('Admin statistics panel actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500);
+ mock
+ .onGet(/api\/(.*)\/application\/statistics/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -99,12 +102,12 @@ describe('Admin statistics panel actions', () => {
it('should commit error', () => {
return testAction(
actions.receiveStatisticsError,
- 500,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
state,
[
{
type: types.RECEIVE_STATISTICS_ERROR,
- payload: 500,
+ payload: HTTP_STATUS_INTERNAL_SERVER_ERROR,
},
],
[],
diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
index 0a3dad09c9a..70c1e723f08 100644
--- a/spec/frontend/admin/statistics_panel/store/mutations_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
@@ -1,6 +1,7 @@
import * as types from '~/admin/statistics_panel/store/mutation_types';
import mutations from '~/admin/statistics_panel/store/mutations';
import getInitialState from '~/admin/statistics_panel/store/state';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
describe('Admin statistics panel mutations', () => {
@@ -30,11 +31,10 @@ describe('Admin statistics panel mutations', () => {
describe(`${types.RECEIVE_STATISTICS_ERROR}`, () => {
it('sets error and clears data', () => {
- const error = 500;
- mutations[types.RECEIVE_STATISTICS_ERROR](state, error);
+ mutations[types.RECEIVE_STATISTICS_ERROR](state, HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(state.isLoading).toBe(false);
- expect(state.error).toBe(error);
+ expect(state.error).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(state.statistics).toEqual(null);
});
});
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index f61af6203f0..738cbd88c4c 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -1,39 +1,66 @@
-import { GlAvatarLabeled, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLabeled, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import TopicSelect from '~/admin/topics/components/topic_select.vue';
+import searchProjectTopics from '~/graphql_shared/queries/project_topics_search.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
const mockTopics = [
- { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' },
- { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+ {
+ id: 'gid://gitlab/Projects::Topic/6',
+ name: 'topic1',
+ title: 'Topic 1',
+ avatarUrl: 'avatar.com/topic1.png',
+ __typename: 'Topic',
+ },
+ {
+ id: 'gid://gitlab/Projects::Topic/5',
+ name: 'gitlab',
+ title: 'GitLab',
+ avatarUrl: 'avatar.com/GitLab.png',
+ __typename: 'Topic',
+ },
];
+const mockTopicsQueryResponse = {
+ data: {
+ topics: {
+ nodes: mockTopics,
+ __typename: 'TopicConnection',
+ },
+ },
+};
+
describe('TopicSelect', () => {
let wrapper;
+ const mockSearchTopicsSuccess = jest.fn().mockResolvedValue(mockTopicsQueryResponse);
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ function createMockApolloProvider({ mockSearchTopicsQuery = mockSearchTopicsSuccess } = {}) {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[searchProjectTopics, mockSearchTopicsQuery]]);
+ }
- function createComponent(props = {}) {
- wrapper = shallowMount(TopicSelect, {
+ function createComponent({ props = {}, mockApollo } = {}) {
+ wrapper = mount(TopicSelect, {
+ apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
data() {
return {
topics: mockTopics,
- search: '',
};
},
- mocks: {
- $apollo: {
- queries: {
- topics: { loading: false },
- },
- },
- },
});
}
afterEach(() => {
wrapper.destroy();
+ jest.clearAllMocks();
});
it('mounts', () => {
@@ -57,17 +84,27 @@ describe('TopicSelect', () => {
it('renders default text if no selected topic', () => {
createComponent();
- expect(findDropdown().props('text')).toBe('Select a topic');
+ expect(findListbox().props('toggleText')).toBe('Select a topic');
});
it('renders selected topic', () => {
- createComponent({ selectedTopic: mockTopics[0] });
+ const mockTopic = mockTopics[0];
- expect(findDropdown().props('text')).toBe('topic1');
+ createComponent({
+ props: {
+ selectedTopic: mockTopic,
+ },
+ });
+
+ expect(findListbox().props('toggleText')).toBe(mockTopic.name);
});
it('renders label', () => {
- createComponent({ labelText: 'my label' });
+ createComponent({
+ props: {
+ labelText: 'my label',
+ },
+ });
expect(wrapper.find('label').text()).toBe('my label');
});
@@ -75,17 +112,52 @@ describe('TopicSelect', () => {
it('renders dropdown items', () => {
createComponent();
- const dropdownItems = findAllDropdownItems();
+ const listboxItems = findAllListboxItems();
+
+ expect(listboxItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1');
+ expect(listboxItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab');
+ });
+
+ it('dropdown `toggledAriaLabelledBy` prop is not set if `labelText` prop is null', () => {
+ createComponent();
- expect(dropdownItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1');
- expect(dropdownItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab');
+ expect(findListbox().props('toggle-aria-labelled-by')).toBe(undefined);
});
- it('emits `click` event when topic selected', () => {
+ it('emits `click` event when topic selected', async () => {
createComponent();
- findAllDropdownItems().at(0).vm.$emit('click');
+ await findAllListboxItems().at(0).trigger('click');
expect(wrapper.emitted('click')).toEqual([[mockTopics[0]]]);
});
+
+ describe('when searching a topic', () => {
+ const searchTopic = (searchTerm) => findListbox().vm.$emit('search', searchTerm);
+ const mockSearchTerm = 'gitl';
+
+ it('toggles loading state', async () => {
+ createComponent();
+ jest.runOnlyPendingTimers();
+
+ await searchTopic(mockSearchTerm);
+
+ expect(findListbox().props('searching')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findListbox().props('searching')).toBe(false);
+ });
+
+ it('fetches topics matching search string', async () => {
+ createComponent();
+
+ await searchTopic(mockSearchTerm);
+ jest.runOnlyPendingTimers();
+
+ expect(mockSearchTopicsSuccess).toHaveBeenCalledWith({
+ search: mockSearchTerm,
+ });
+ });
+ });
});
diff --git a/spec/frontend/airflow/dags/components/dags_spec.js b/spec/frontend/airflow/dags/components/dags_spec.js
new file mode 100644
index 00000000000..f9cf4fc87af
--- /dev/null
+++ b/spec/frontend/airflow/dags/components/dags_spec.js
@@ -0,0 +1,115 @@
+import { GlAlert, GlPagination, GlTableLite } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import AirflowDags from '~/airflow/dags/components/dags.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { mockDags } from './mock_data';
+
+describe('AirflowDags', () => {
+ let wrapper;
+
+ const createWrapper = (
+ dags = [],
+ pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ ) => {
+ wrapper = mountExtended(AirflowDags, {
+ propsData: {
+ dags,
+ pagination,
+ },
+ });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEmptyState = () => wrapper.findByText('There are no DAGs to show');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ describe('default (no dags)', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows incubation warning', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('shows empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('with dags', () => {
+ const createWrapperWithDags = (pagination = {}) => {
+ createWrapper(mockDags, {
+ page: 1,
+ isLastPage: false,
+ per_page: 2,
+ totalItems: 5,
+ ...pagination,
+ });
+ };
+
+ const findDagsData = () => {
+ return wrapper
+ .findComponent(GlTableLite)
+ .findAll('tbody tr')
+ .wrappers.map((tr) => {
+ return tr.findAll('td').wrappers.map((td) => {
+ const timeAgo = td.findComponent(TimeAgo);
+
+ if (timeAgo.exists()) {
+ return {
+ type: 'time',
+ value: timeAgo.props('time'),
+ };
+ }
+
+ return {
+ type: 'text',
+ value: td.text(),
+ };
+ });
+ });
+ };
+
+ it('renders the table of Dags with data', () => {
+ createWrapperWithDags();
+
+ expect(findDagsData()).toEqual(
+ mockDags.map((x) => [
+ { type: 'text', value: x.dag_name },
+ { type: 'text', value: x.schedule },
+ { type: 'time', value: x.next_run },
+ { type: 'text', value: String(x.is_active) },
+ { type: 'text', value: String(x.is_paused) },
+ { type: 'text', value: x.fileloc },
+ ]),
+ );
+ });
+
+ describe('Pagination behaviour', () => {
+ it.each`
+ pagination | expected
+ ${{}} | ${{ value: 1, prevPage: null, nextPage: 2 }}
+ ${{ page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: 3 }}
+ ${{ isLastPage: true, page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: null }}
+ `('with $pagination, sets pagination props', ({ pagination, expected }) => {
+ createWrapperWithDags({ ...pagination });
+
+ expect(findPagination().props()).toMatchObject(expected);
+ });
+
+ it('generates link for each page', () => {
+ createWrapperWithDags();
+
+ const generateLink = findPagination().props('linkGen');
+
+ expect(generateLink(3)).toBe(`${TEST_HOST}/?page=3`);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/airflow/dags/components/mock_data.js b/spec/frontend/airflow/dags/components/mock_data.js
new file mode 100644
index 00000000000..9547282517d
--- /dev/null
+++ b/spec/frontend/airflow/dags/components/mock_data.js
@@ -0,0 +1,67 @@
+export const mockDags = [
+ {
+ id: 1,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 1',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 2,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 2',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 3,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 3',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 4,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 4',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 5,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 5',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+];
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 bff4905a12c..0e402e61bcc 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
@@ -89,6 +89,7 @@ exports[`Alert integration settings form default state should match the default
optionaltext="(optional)"
>
<gl-form-checkbox-stub
+ data-qa-selector="enable_email_notification_checkbox"
id="3"
>
<span>
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index 6a58f8c6d29..e0bfff3e664 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -1,6 +1,7 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
+import { METRIC_POPOVER_LABEL } from '~/analytics/shared/constants';
const MOCK_METRIC = {
key: 'deployment-frequency',
@@ -27,10 +28,11 @@ describe('MetricPopover', () => {
};
const findMetricLabel = () => wrapper.findByTestId('metric-label');
- const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
+ const findMetricLink = () => wrapper.find('[data-testid="metric-link"]');
const findMetricDescription = () => wrapper.findByTestId('metric-description');
const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
+ const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
@@ -47,17 +49,14 @@ describe('MetricPopover', () => {
});
describe('with links', () => {
+ const METRIC_NAME = 'Deployment frequency';
+ const LINK_URL = '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency';
const links = [
{
- name: 'Deployment frequency',
- url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency',
+ name: METRIC_NAME,
+ url: LINK_URL,
label: 'Dashboard',
},
- {
- name: 'Another link',
- url: '/groups/gitlab-org/-/analytics/another-link',
- label: 'Another link',
- },
];
const docsLink = {
name: 'Deployment frequency',
@@ -68,37 +67,34 @@ describe('MetricPopover', () => {
const linksWithDocs = [...links, docsLink];
describe.each`
- hasDocsLink | allLinks | displayedMetricLinks
- ${true} | ${linksWithDocs} | ${links}
- ${false} | ${links} | ${links}
- `(
- 'when one link has docs_link=$hasDocsLink',
- ({ hasDocsLink, allLinks, displayedMetricLinks }) => {
- beforeEach(() => {
- wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
- });
+ hasDocsLink | allLinks
+ ${true} | ${linksWithDocs}
+ ${false} | ${links}
+ `('when one link has docs_link=$hasDocsLink', ({ hasDocsLink, allLinks }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
+ });
- displayedMetricLinks.forEach((link, idx) => {
- it(`renders a link for "${link.name}"`, () => {
- const allLinkContainers = findAllMetricLinks();
+ describe('Metric title row', () => {
+ it(`renders a link for "${METRIC_NAME}"`, () => {
+ expect(findMetricLink().text()).toContain(METRIC_POPOVER_LABEL);
+ expect(findMetricLink().findComponent(GlLink).attributes('href')).toBe(LINK_URL);
+ });
- expect(allLinkContainers.at(idx).text()).toContain(link.name);
- expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe(
- link.url,
- );
- });
+ it('renders the chart icon', () => {
+ expect(findMetricDetailsIcon().attributes('name')).toBe('chart');
});
+ });
- it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
- expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
+ it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
+ expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
- if (hasDocsLink) {
- expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
- expect(findMetricDocsLink().text()).toBe(docsLink.label);
- expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
- }
- });
- },
- );
+ if (hasDocsLink) {
+ expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
+ expect(findMetricDocsLink().text()).toBe(docsLink.label);
+ expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
+ }
+ });
+ });
});
});
diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js
index aac14e64286..507f659a170 100644
--- a/spec/frontend/api/alert_management_alerts_api_spec.js
+++ b/spec/frontend/api/alert_management_alerts_api_spec.js
@@ -1,6 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
describe('~/api/alert_management_alerts_api.js', () => {
let mock;
@@ -33,7 +38,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
const expectedData = [imageData];
const options = { alertIid, id: projectId };
- mock.onGet(expectedUrl).reply(200, { data: expectedData });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedData });
return alertManagementAlertsApi.fetchAlertMetricImages(options).then(({ data }) => {
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
@@ -60,7 +65,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
expectedFormData.append('url', url);
expectedFormData.append('url_text', urlText);
- mock.onPost(expectedUrl).reply(201, { data: expectedData });
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_CREATED, { data: expectedData });
return alertManagementAlertsApi
.uploadAlertMetricImage({
@@ -96,7 +101,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
expectedFormData.append('url', url);
expectedFormData.append('url_text', urlText);
- mock.onPut(expectedUrl).reply(200, { data: expectedData });
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedData });
return alertManagementAlertsApi
.updateAlertMetricImage({
@@ -123,7 +128,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
const expectedData = [imageData];
- mock.onDelete(expectedUrl).reply(204, { data: expectedData });
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_NO_CONTENT, { data: expectedData });
return alertManagementAlertsApi
.deleteAlertMetricImage({
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index c354d8a9416..0315db02cf2 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -3,7 +3,7 @@ import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_PER_PAGE } from '~/api';
-import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
+import { updateGroup, getGroupTransferLocations, getGroupMembers } from '~/api/groups_api';
const mockApiVersion = 'v4';
const mockUrlRoot = '/gitlab';
@@ -55,7 +55,9 @@ describe('GroupsApi', () => {
const params = { page: 1 };
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/transfer_locations`;
- mock.onGet(expectedUrl).replyOnce(200, { data: getGroupTransferLocationsResponse });
+ mock
+ .onGet(expectedUrl)
+ .replyOnce(HTTP_STATUS_OK, { data: getGroupTransferLocationsResponse });
await expect(getGroupTransferLocations(mockGroupId, params)).resolves.toMatchObject({
data: { data: getGroupTransferLocationsResponse },
@@ -66,4 +68,30 @@ describe('GroupsApi', () => {
});
});
});
+
+ describe('getGroupMembers', () => {
+ it('requests members of a group', async () => {
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(getGroupMembers(mockGroupId)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+
+ it('requests inherited members of a group when requested', async () => {
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members/all`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(getGroupMembers(mockGroupId, true)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 8459021421f..2d4ed39dad0 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -3,18 +3,22 @@ import getTransferLocationsResponse from 'test_fixtures/api/projects/transfer_lo
import * as projectsApi from '~/api/projects_api';
import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/projects_api.js', () => {
let mock;
let originalGon;
const projectId = 1;
+ const setfullPathProjectSearch = (value) => {
+ window.gon.features.fullPathProjectSearch = value;
+ };
beforeEach(() => {
mock = new MockAdapter(axios);
originalGon = window.gon;
- window.gon = { api_version: 'v7' };
+ window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } };
});
afterEach(() => {
@@ -27,14 +31,53 @@ describe('~/api/projects_api.js', () => {
jest.spyOn(axios, 'get');
});
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedProjects = [{ name: 'project 1' }];
+ const options = {};
+
it('retrieves projects from the correct URL and returns them in the response data', () => {
- const expectedUrl = '/api/v7/projects.json';
const expectedParams = { params: { per_page: 20, search: '', simple: true } };
- const expectedProjects = [{ name: 'project 1' }];
const query = '';
- const options = {};
- mock.onGet(expectedUrl).reply(200, { data: expectedProjects });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(query, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('omits search param if query is undefined', () => {
+ const expectedParams = { params: { per_page: 20, simple: true } };
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(undefined, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('searches namespaces if query contains a slash', () => {
+ const expectedParams = {
+ params: { per_page: 20, search: 'group/project1', search_namespaces: true, simple: true },
+ };
+ const query = 'group/project1';
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(query, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('does not search namespaces if fullPathProjectSearch is disabled', () => {
+ setfullPathProjectSearch(false);
+ const expectedParams = { params: { per_page: 20, search: 'group/project1', simple: true } };
+ const query = 'group/project1';
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
return projectsApi.getProjects(query, options).then(({ data }) => {
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
@@ -53,7 +96,7 @@ describe('~/api/projects_api.js', () => {
const expectedUrl = '/api/v7/projects/1/import_project_members/2';
const expectedMessage = 'Successfully imported';
- mock.onPost(expectedUrl).replyOnce(200, expectedMessage);
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedMessage);
return projectsApi.importProjectMembers(projectId, targetId).then(({ data }) => {
expect(axios.post).toHaveBeenCalledWith(expectedUrl);
@@ -71,7 +114,7 @@ describe('~/api/projects_api.js', () => {
const params = { page: 1 };
const expectedUrl = '/api/v7/projects/1/transfer_locations';
- mock.onGet(expectedUrl).replyOnce(200, { data: getTransferLocationsResponse });
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, { data: getTransferLocationsResponse });
await expect(projectsApi.getTransferLocations(projectId, params)).resolves.toMatchObject({
data: { data: getTransferLocationsResponse },
@@ -82,4 +125,30 @@ describe('~/api/projects_api.js', () => {
});
});
});
+
+ describe('getProjectMembers', () => {
+ it('requests members of a project', async () => {
+ const expectedUrl = `/api/v7/projects/1/members`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(projectsApi.getProjectMembers(projectId)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+
+ it('requests inherited members of a project when requested', async () => {
+ const expectedUrl = `/api/v7/projects/1/members/all`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(projectsApi.getProjectMembers(projectId, true)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index 9e901cf0f71..4d0252aad23 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { followUser, unfollowUser, associationsCount, updateUserStatus } from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
associationsCount as associationsCountData,
userStatus as mockUserStatus,
@@ -31,7 +32,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/follow';
const expectedResponse = { message: 'Success' };
- axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(followUser(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -45,7 +46,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/unfollow';
const expectedResponse = { message: 'Success' };
- axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(unfollowUser(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -59,7 +60,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/associations_count';
const expectedResponse = { data: associationsCountData };
- axiosMock.onGet(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(associationsCount(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -79,7 +80,7 @@ describe('~/api/user_api', () => {
};
const expectedResponse = { data: mockUserStatus };
- axiosMock.onPatch(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPatch(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(
updateUserStatus({
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 39fbe02480d..6fd106502c4 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -206,7 +206,7 @@ describe('Api', () => {
expires_at: undefined,
};
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -478,7 +478,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -494,7 +494,7 @@ describe('Api', () => {
const projectId = 1;
const options = { state: 'active' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
id: 1,
title: 'milestone1',
@@ -514,7 +514,7 @@ describe('Api', () => {
const projectId = 1;
const issueIid = 11;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/issues/11/todo`;
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
id: 112,
project: {
id: 1,
@@ -541,7 +541,7 @@ describe('Api', () => {
expires_at: undefined,
};
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -625,7 +625,7 @@ describe('Api', () => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(500, null);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, null);
const apiCall = Api.groupProjects(groupId, query, {});
await expect(apiCall).rejects.toThrow();
});
@@ -644,7 +644,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -958,7 +958,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(200, [
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, [
{
id: 'abcdefghijklmnop',
short_id: 'abcdefg',
@@ -984,7 +984,9 @@ describe('Api', () => {
mock
.onGet(expectedUrl)
- .replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
+ .replyOnce(HTTP_STATUS_OK, [
+ { id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' },
+ ]);
return Api.allContextCommits(projectPath, mergeRequestId).then(({ data }) => {
expect(data[0].title).toBe('Dummy commit title');
@@ -1004,7 +1006,7 @@ describe('Api', () => {
jest.spyOn(axios, 'delete');
- mock.onDelete(expectedUrl).replyOnce(204);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_NO_CONTENT);
return Api.removeContextCommits(projectPath, mergeRequestId, expectedData).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/artifacts/components/app_spec.js
new file mode 100644
index 00000000000..931c4703e95
--- /dev/null
+++ b/spec/frontend/artifacts/components/app_spec.js
@@ -0,0 +1,109 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import ArtifactsApp from '~/artifacts/components/app.vue';
+import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/artifacts/constants';
+
+const TEST_BUILD_ARTIFACTS_SIZE = 1024;
+const TEST_PROJECT_PATH = 'project/path';
+const TEST_PROJECT_ID = 'gid://gitlab/Project/22';
+
+const createBuildArtifactsSizeResponse = (buildArtifactsSize) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ statistics: {
+ __typename: 'ProjectStatistics',
+ buildArtifactsSize,
+ },
+ },
+ },
+});
+
+Vue.use(VueApollo);
+
+describe('ArtifactsApp component', () => {
+ let wrapper;
+ let apolloProvider;
+ let getBuildArtifactsSizeSpy;
+
+ const findTitle = () => wrapper.findByTestId('artifacts-page-title');
+ const findBuildArtifactsSize = () => wrapper.findByTestId('build-artifacts-size');
+ const findJobArtifactsTable = () => wrapper.findComponent(JobArtifactsTable);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ArtifactsApp, {
+ provide: { projectPath: 'project/path' },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ getBuildArtifactsSizeSpy = jest.fn();
+
+ apolloProvider = createMockApollo([[getBuildArtifactsSizeQuery, getBuildArtifactsSizeSpy]]);
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ // Promise that never resolves so it's always loading
+ getBuildArtifactsSizeSpy.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ it('shows the page title', () => {
+ expect(findTitle().text()).toBe(PAGE_TITLE);
+ });
+
+ it('shows a skeleton while loading the artifacts size', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('shows the job artifacts table', () => {
+ expect(findJobArtifactsTable().exists()).toBe(true);
+ });
+
+ it('does not show message', () => {
+ expect(findBuildArtifactsSize().text()).toBe('');
+ });
+
+ it('calls apollo query', () => {
+ expect(getBuildArtifactsSizeSpy).toHaveBeenCalledWith({ projectPath: TEST_PROJECT_PATH });
+ });
+ });
+
+ describe.each`
+ buildArtifactsSize | expectedText
+ ${TEST_BUILD_ARTIFACTS_SIZE} | ${numberToHumanSize(TEST_BUILD_ARTIFACTS_SIZE)}
+ ${null} | ${SIZE_UNKNOWN}
+ `('when buildArtifactsSize is $buildArtifactsSize', ({ buildArtifactsSize, expectedText }) => {
+ beforeEach(async () => {
+ getBuildArtifactsSizeSpy.mockResolvedValue(
+ createBuildArtifactsSizeResponse(buildArtifactsSize),
+ );
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('hides loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('shows the size', () => {
+ expect(findBuildArtifactsSize().text()).toMatchInterpolatedText(
+ `${TOTAL_ARTIFACTS_SIZE} ${expectedText}`,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index 7ae3a2734cb..23d1e5c7dee 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import U2FRegister from '~/authentication/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
@@ -24,7 +25,7 @@ describe('U2FRegister', () => {
it('allows registering a U2F device', () => {
const setupButton = container.find('#js-setup-token-2fa-device');
- expect(setupButton.text()).toBe('Set up new device');
+ expect(trimText(setupButton.text())).toBe('Set up new device');
setupButton.trigger('click');
const inProgressMessage = container.children('p');
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 95cb993fc70..773481346fc 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnRegister from '~/authentication/webauthn/register';
@@ -52,7 +53,7 @@ describe('WebAuthnRegister', () => {
const findRetryButton = () => container.find('#js-token-2fa-try-again');
it('shows setup button', () => {
- expect(findSetupButton().text()).toBe('Set up new device');
+ expect(trimText(findSetupButton().text())).toBe('Set up new device');
});
describe('when unsupported', () => {
diff --git a/spec/frontend/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index b799273ff63..5ca199357f9 100644
--- a/spec/frontend/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -5,6 +5,7 @@ import actions, { transformBackendBadge } from '~/badges/store/actions';
import mutationTypes from '~/badges/store/mutation_types';
import createState from '~/badges/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge';
describe('Badges store actions', () => {
@@ -98,7 +99,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
dispatch.mockClear();
- return [200, dummyResponse];
+ return [HTTP_STATUS_OK, dummyResponse];
});
const dummyBadge = transformBackendBadge(dummyResponse);
@@ -119,7 +120,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.addBadge({ state, dispatch })).rejects.toThrow();
@@ -176,7 +177,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
- return [200, ''];
+ return [HTTP_STATUS_OK, ''];
});
await actions.deleteBadge({ state, dispatch }, { id: badgeId });
@@ -187,7 +188,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.deleteBadge({ state, dispatch }, { id: badgeId })).rejects.toThrow();
@@ -265,7 +266,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
dispatch.mockClear();
- return [200, dummyReponse];
+ return [HTTP_STATUS_OK, dummyReponse];
});
await actions.loadBadges({ state, dispatch }, dummyData);
@@ -279,7 +280,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.loadBadges({ state, dispatch }, dummyData)).rejects.toThrow();
@@ -380,7 +381,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
- return [200, dummyReponse];
+ return [HTTP_STATUS_OK, dummyReponse];
});
await actions.renderBadge({ state, dispatch });
@@ -393,7 +394,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.renderBadge({ state, dispatch })).rejects.toThrow();
@@ -467,7 +468,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
dispatch.mockClear();
- return [200, dummyResponse];
+ return [HTTP_STATUS_OK, dummyResponse];
});
const updatedBadge = transformBackendBadge(dummyResponse);
@@ -487,7 +488,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.saveBadge({ state, dispatch })).rejects.toThrow();
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 2dfcdd551a1..924d88866ee 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,9 +1,8 @@
import { nextTick } from 'vue';
-import { GlButton, GlBadge } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
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 { createDraft } from '../mock_data';
@@ -30,9 +29,6 @@ describe('Batch comments draft note component', () => {
},
};
- const findSubmitReviewButton = () => wrapper.findComponent(PublishButton);
- const findAddCommentButton = () => wrapper.findComponent(GlButton);
-
const createComponent = (propsData = { draft }, glFeatures = {}) => {
wrapper = shallowMount(DraftNote, {
store,
@@ -67,58 +63,6 @@ describe('Batch comments draft note component', () => {
expect(note.props().note).toEqual(draft);
});
- describe('add comment now', () => {
- it('dispatches publishSingleDraft when clicking', () => {
- createComponent();
- const publishNowButton = findAddCommentButton();
- publishNowButton.vm.$emit('click');
-
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
- 'batchComments/publishSingleDraft',
- 1,
- );
- });
-
- it('sets as loading when draft is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
-
- await nextTick();
- const publishNowButton = findAddCommentButton();
-
- expect(publishNowButton.props().loading).toBe(true);
- });
-
- it('sets as disabled when review is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.isPublishing = true;
-
- await nextTick();
- const publishNowButton = findAddCommentButton();
-
- expect(publishNowButton.props().disabled).toBe(true);
- expect(publishNowButton.props().loading).toBe(false);
- });
-
- it('hides button when mr_review_submit_comment is enabled', () => {
- createComponent({ draft }, { mrReviewSubmitComment: true });
-
- expect(findAddCommentButton().exists()).toBe(false);
- });
- });
-
- describe('submit review', () => {
- it('sets as disabled when draft is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
-
- await nextTick();
- const publishNowButton = findSubmitReviewButton();
-
- expect(publishNowButton.attributes().disabled).toBe('true');
- });
- });
-
describe('update', () => {
it('dispatches updateDraft', async () => {
createComponent();
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 283632cb560..f86e003ab5f 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -1,9 +1,11 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
+import PreviewItem from '~/batch_comments/components/preview_item.vue';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -17,6 +19,8 @@ let wrapper;
const setCurrentFileHash = jest.fn();
const scrollToDraft = jest.fn();
+const findPreviewItem = () => wrapper.findComponent(PreviewItem);
+
function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = [] } = {}) {
const store = new Vuex.Store({
modules: {
@@ -42,16 +46,13 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
},
});
- wrapper = shallowMountExtended(PreviewDropdown, {
+ wrapper = shallowMount(PreviewDropdown, {
store,
+ stubs: { GlDisclosureDropdown },
});
}
describe('Batch comments preview dropdown', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('clicking draft', () => {
it('toggles active file when viewDiffsFileByFile is true', async () => {
factory({
@@ -59,12 +60,15 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, file_hash: 'hash' }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
- expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1, file_hash: 'hash' });
+ expect(scrollToDraft).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ id: 1, file_hash: 'hash' }),
+ );
});
it('calls scrollToDraft', async () => {
@@ -73,11 +77,14 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1 }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
- expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 });
+ expect(scrollToDraft).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ id: 1 }),
+ );
});
it('changes window location to navigate to commit', async () => {
@@ -86,7 +93,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js
deleted file mode 100644
index 5e3fa3e9446..00000000000
--- a/spec/frontend/batch_comments/components/publish_button_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
-import PublishButton from '~/batch_comments/components/publish_button.vue';
-import { createStore } from '~/batch_comments/stores';
-
-describe('Batch comments publish button component', () => {
- let wrapper;
- let store;
-
- beforeEach(() => {
- store = createStore();
-
- wrapper = mount(PublishButton, { store, propsData: { shouldPublish: true } });
-
- jest.spyOn(store, 'dispatch').mockImplementation();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches publishReview on click', async () => {
- await wrapper.trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
- });
-
- it('sets loading when isPublishing is true', async () => {
- store.state.batchComments.isPublishing = true;
-
- await nextTick();
- expect(wrapper.attributes('disabled')).toBe('disabled');
- });
-});
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index e89934c0192..44d7b56c14f 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -12,29 +12,30 @@ Vue.use(Vuex);
describe('Batch comments publish dropdown component', () => {
let wrapper;
+ const draft = createDraft();
function createComponent() {
const store = createStore();
- store.state.batchComments.drafts.push(createDraft(), { ...createDraft(), id: 2 });
+ store.state.batchComments.drafts.push(draft, { ...draft, id: 2 });
wrapper = shallowMount(PreviewDropdown, {
store,
+ stubs: { GlDisclosureDropdown },
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list of drafts', () => {
createComponent();
- expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2);
+ expect(wrapper.findComponent(GlDisclosureDropdown).props('items')).toMatchObject([
+ draft,
+ { ...draft, id: 2 },
+ ]);
});
it('renders draft count in dropdown title', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdown).props('headerText')).toEqual('2 pending comments');
+ expect(wrapper.findComponent(GlDisclosureDropdown).text()).toEqual('2 pending comments');
});
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 6369ea9aa15..20eedcbb25b 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import service from '~/batch_comments/services/drafts_service';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Batch comments store actions', () => {
let res = {};
@@ -31,7 +32,7 @@ describe('Batch comments store actions', () => {
describe('addDraftToDiscussion', () => {
it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.addDraftToDiscussion,
@@ -43,7 +44,7 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.addDraftToDiscussion,
@@ -58,7 +59,7 @@ describe('Batch comments store actions', () => {
describe('createNewDraft', () => {
it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.createNewDraft,
@@ -70,7 +71,7 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.createNewDraft,
@@ -100,7 +101,7 @@ describe('Batch comments store actions', () => {
commit,
};
res = { id: 1 };
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
return actions.deleteDraft(context, { id: 1 }).then(() => {
expect(commit).toHaveBeenCalledWith('DELETE_DRAFT', 1);
@@ -113,7 +114,7 @@ describe('Batch comments store actions', () => {
getters,
commit,
};
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return actions.deleteDraft(context, { id: 1 }).then(() => {
expect(commit).not.toHaveBeenCalledWith('DELETE_DRAFT', 1);
@@ -144,7 +145,7 @@ describe('Batch comments store actions', () => {
},
};
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return actions.fetchDrafts(context).then(() => {
expect(commit).toHaveBeenCalledWith('SET_BATCH_COMMENTS_DRAFTS', { id: 1 });
@@ -169,7 +170,7 @@ describe('Batch comments store actions', () => {
});
it('dispatches actions & commits', () => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
@@ -180,7 +181,7 @@ describe('Batch comments store actions', () => {
});
it('calls service with notes data', () => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
jest.spyOn(axios, 'post');
return actions
@@ -191,7 +192,7 @@ describe('Batch comments store actions', () => {
});
it('dispatches error commits', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return actions.publishReview({ dispatch, commit, getters, rootGetters }).catch(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
@@ -221,7 +222,7 @@ describe('Batch comments store actions', () => {
commit,
};
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
params = { note: { id: 1 }, noteText: 'test' };
});
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 5fe328b65ff..6af9cdcae7d 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -10,6 +10,8 @@ function createComponent() {
wrapper = shallowMount(TableContents);
}
+const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+
async function setLoaded(loaded) {
document.querySelector('.blob-viewer').dataset.loaded = loaded;
@@ -20,10 +22,10 @@ describe('Markdown table of contents component', () => {
beforeEach(() => {
setHTMLFixture(`
<div class="blob-viewer" data-type="rich" data-loaded="false">
- <h1><a href="#1"></a>Hello</h1>
- <h2><a href="#2"></a>World</h2>
- <h3><a href="#3"></a>Testing</h3>
- <h2><a href="#4"></a>GitLab</h2>
+ <h1><a id="hello">$</a> Hello</h1>
+ <h2><a id="world">$</a> World</h2>
+ <h3><a id="hakuna">$</a> Hakuna</h3>
+ <h2><a id="matata">$</a> Matata</h2>
</div>
`);
});
@@ -34,12 +36,10 @@ describe('Markdown table of contents component', () => {
});
describe('not loaded', () => {
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
-
it('does not populate dropdown', () => {
createComponent();
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it('does not show dropdown when loading blob content', async () => {
@@ -47,7 +47,7 @@ describe('Markdown table of contents component', () => {
await setLoaded(false);
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it('does not show dropdown when viewing non-rich content', async () => {
@@ -57,7 +57,7 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
});
@@ -67,15 +67,25 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+ const dropdown = findDropdown();
- expect(dropdownItems.exists()).toBe(true);
- expect(dropdownItems.length).toBe(4);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('items').length).toBe(4);
// make sure that this only happens once
await setLoaded(true);
- expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(4);
+ expect(dropdown.props('items').length).toBe(4);
+ });
+
+ it('generates proper anchor links', async () => {
+ createComponent();
+ await setLoaded(true);
+
+ const dropdown = findDropdown();
+ const items = dropdown.props('items');
+ const hrefs = items.map((item) => item.href);
+ expect(hrefs).toEqual(['#hello', '#world', '#hakuna', '#matata']);
});
it('sets padding for dropdown items', async () => {
@@ -83,12 +93,12 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- const dropdownLinks = wrapper.findAll('[data-testid="tableContentsLink"]');
+ const items = findDropdown().props('items');
- expect(dropdownLinks.at(0).element.style.paddingLeft).toBe('0px');
- expect(dropdownLinks.at(1).element.style.paddingLeft).toBe('8px');
- expect(dropdownLinks.at(2).element.style.paddingLeft).toBe('16px');
- expect(dropdownLinks.at(3).element.style.paddingLeft).toBe('8px');
+ expect(items[0].extraAttrs.style.paddingLeft).toBe('16px');
+ expect(items[1].extraAttrs.style.paddingLeft).toBe('24px');
+ expect(items[2].extraAttrs.style.paddingLeft).toBe('32px');
+ expect(items[3].extraAttrs.style.paddingLeft).toBe('24px');
});
});
});
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index ea4badc03fb..2e7eadc912d 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/blob/notebook/notebook_viewer.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NotebookLab from '~/notebook/index.vue';
describe('iPython notebook renderer', () => {
@@ -54,7 +55,7 @@ describe('iPython notebook renderer', () => {
describe('successful response', () => {
beforeEach(() => {
- mock.onGet(endpoint).reply(200, mockNotebook);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, mockNotebook);
mountComponent();
return waitForPromises();
});
@@ -72,7 +73,7 @@ describe('iPython notebook renderer', () => {
beforeEach(() => {
mock.onGet(endpoint).reply(() =>
// eslint-disable-next-line prefer-promise-reject-errors
- Promise.reject({ status: 200 }),
+ Promise.reject({ status: HTTP_STATUS_OK }),
);
mountComponent();
@@ -90,7 +91,7 @@ describe('iPython notebook renderer', () => {
describe('error getting file', () => {
beforeEach(() => {
- mock.onGet(endpoint).reply(500, '');
+ mock.onGet(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
mountComponent();
return waitForPromises();
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index d9d65258516..95e86398ab8 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -1,7 +1,9 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import renderOpenApi from '~/blob/openapi';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('OpenAPI blob viewer', () => {
const id = 'js-openapi-viewer';
@@ -10,7 +12,7 @@ describe('OpenAPI blob viewer', () => {
beforeEach(async () => {
setHTMLFixture(`<div id="${id}" data-endpoint="${mockEndpoint}"></div>`);
- mock = new MockAdapter(axios).onGet().reply(200);
+ mock = new MockAdapter(axios).onGet().reply(HTTP_STATUS_OK);
await renderOpenApi();
});
@@ -21,7 +23,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 allow-forms" frameborder="0" width="100%" height="1000"></iframe>',
+ `<iframe src="${TEST_HOST}/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>`,
);
});
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 2c8e6306431..1e823e3321a 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,16 +1,18 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
+import { TYPE_ISSUE } from '~/issues/constants';
import { updateHistory } from '~/lib/utils/url_utility';
import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data';
@@ -18,6 +20,7 @@ jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
Vue.use(Vuex);
+Vue.use(VueApollo);
describe('Board card component', () => {
const user = {
@@ -69,6 +72,7 @@ describe('Board card component', () => {
const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
+ apolloProvider: createMockApollo(),
propsData: {
list,
item: issue,
@@ -82,18 +86,11 @@ describe('Board card component', () => {
directives: {
GlTooltip: createMockDirective(),
},
- mocks: {
- $apollo: {
- queries: {
- blockingIssuables: { loading: false },
- },
- },
- },
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
isGroupBoard,
},
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 1ba546f24a8..d882ff071b7 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -22,6 +22,7 @@ export default function createComponent({
listIssueProps = {},
componentProps = {},
listProps = {},
+ apolloQueryHandlers = [],
actions = {},
getters = {},
provide = {},
@@ -39,6 +40,7 @@ export default function createComponent({
const fakeApollo = createMockApollo([
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
+ ...apolloQueryHandlers,
]);
const store = new Vuex.Store({
@@ -89,6 +91,7 @@ export default function createComponent({
list,
boardItems: [issue],
canAdminList: true,
+ boardId: 'gid://gitlab/Board/1',
...componentProps,
},
provide: {
@@ -104,6 +107,9 @@ export default function createComponent({
isGroupBoard: false,
isProjectBoard: true,
disabled: false,
+ boardType: 'group',
+ issuableType: 'issue',
+ isApolloBoard: false,
...provide,
},
stubs,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index abe8c230bd8..fc8dbf8dc3a 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,6 +1,6 @@
import Draggable from 'vuedraggable';
import { nextTick } from 'vue';
-import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
+import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import waitForPromises from 'helpers/wait_for_promises';
import createComponent from 'jest/boards/board_list_helper';
@@ -107,6 +107,20 @@ describe('Board list component', () => {
});
});
+ describe('when ListType is Closed', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: {
+ listType: ListType.closed,
+ },
+ });
+ });
+
+ it('Board card move to position is not visible', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(false);
+ });
+ });
+
describe('load more issues', () => {
const actions = {
fetchItemsForList: jest.fn(),
@@ -159,27 +173,32 @@ describe('Board list component', () => {
});
describe('when issue count exceeds max issue count', () => {
- it('sets background to bg-danger-100', async () => {
+ it('sets background to gl-bg-red-100', async () => {
wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
await nextTick();
- expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
+ const block = wrapper.find('.gl-bg-red-100');
+
+ expect(block.exists()).toBe(true);
+ expect(block.attributes('class')).toContain(
+ 'gl-rounded-bottom-left-base gl-rounded-bottom-right-base',
+ );
});
});
describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to bg-danger-100', () => {
+ it('does not sets background to gl-bg-red-100', () => {
wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
+ expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
describe('when list max issue count is 0', () => {
- it('does not sets background to bg-danger-100', () => {
+ it('does not sets background to gl-bg-red-100', () => {
wrapper.setProps({ list: { maxIssueCount: 0 } });
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
+ expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 872a67a71fb..12318fb5d16 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -27,7 +27,7 @@ describe('BoardApp', () => {
wrapper = shallowMount(BoardApp, {
store,
provide: {
- fullBoardId: 'gid://gitlab/Board/1',
+ initialBoardId: 'gid://gitlab/Board/1',
},
});
};
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
index 8dee3c77787..8af772ba6d0 100644
--- a/spec/frontend/boards/components/board_card_move_to_position_spec.js
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -1,8 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import {
+ BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION,
+ BOARD_CARD_MOVE_TO_POSITIONS_END_OPTION,
+} from '~/boards/constants';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -10,8 +13,14 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
Vue.use(Vuex);
const dropdownOptions = [
- BoardCardMoveToPosition.i18n.moveToStartText,
- BoardCardMoveToPosition.i18n.moveToEndText,
+ {
+ text: BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION,
+ action: jest.fn(),
+ },
+ {
+ text: BOARD_CARD_MOVE_TO_POSITIONS_END_OPTION,
+ action: jest.fn(),
+ },
];
describe('Board Card Move to position', () => {
@@ -53,8 +62,8 @@ describe('Board Card Move to position', () => {
...propsData,
},
stubs: {
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
},
});
};
@@ -64,12 +73,9 @@ describe('Board Card Move to position', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem);
+ const findMoveToPositionDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () =>
+ findMoveToPositionDropdown().findAllComponents(GlDisclosureDropdownItem);
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
describe('Dropdown', () => {
@@ -80,7 +86,7 @@ describe('Board Card Move to position', () => {
});
it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => {
- findMoveToPositionDropdown().vm.$emit('click');
+ findMoveToPositionDropdown().vm.$emit('shown');
expect(findDropdownItems()).toHaveLength(dropdownOptions.length);
});
});
@@ -97,26 +103,24 @@ describe('Board Card Move to position', () => {
});
it.each`
- dropdownIndex | dropdownLabel | trackLabel | positionInList
- ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0}
- ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1}
+ dropdownIndex | dropdownItem | trackLabel | positionInList
+ ${0} | ${dropdownOptions[0]} | ${'move_to_start'} | ${0}
+ ${1} | ${dropdownOptions[1]} | ${'move_to_end'} | ${-1}
`(
'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel',
- async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => {
- await findMoveToPositionDropdown().vm.$emit('click');
+ async ({ dropdownIndex, dropdownItem, trackLabel, positionInList }) => {
+ await findMoveToPositionDropdown().vm.$emit('shown');
- expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel);
- await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', {
- stopPropagation: () => {},
- });
+ expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownItem.text);
- await nextTick();
+ await findMoveToPositionDropdown().vm.$emit('action', dropdownItem);
expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', {
category: 'boards:list',
label: trackLabel,
property: 'type_card',
});
+
expect(dispatch).toHaveBeenCalledWith('moveItem', {
fromListId: mockList.id,
itemId: mockIssue2.id,
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index f8ad7c468c1..84e6318d98e 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -61,6 +61,7 @@ describe('Board card', () => {
isProjectBoard: false,
isGroupBoard: true,
disabled: false,
+ isApolloBoard: false,
...provide,
},
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index d34e228a2d7..c0bb51620f2 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -35,6 +35,10 @@ describe('Board Column Component', () => {
store,
propsData: {
list: listMock,
+ boardId: 'gid://gitlab/Board/1',
+ },
+ provide: {
+ isApolloBoard: false,
},
});
};
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 51c42b48535..955267a415c 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -7,9 +7,10 @@ import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdow
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
-import { ISSUABLE, issuableTypes } from '~/boards/constants';
+import { ISSUABLE } from '~/boards/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
-import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
+import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.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 '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
@@ -53,7 +54,7 @@ describe('BoardContentSidebar', () => {
canUpdate: true,
rootPath: '/',
groupId: 1,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
isGroupBoard: false,
},
store,
@@ -142,8 +143,8 @@ describe('BoardContentSidebar', () => {
);
});
- it('does not render SidebarSeverity', () => {
- expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
+ it('does not render SidebarSeverityWidget', () => {
+ expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(false);
});
it('does not render SidebarHealthStatusWidget', async () => {
@@ -188,8 +189,8 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- it('renders SidebarSeverity', () => {
- expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(true);
+ it('renders SidebarSeverityWidget', () => {
+ expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index a16b99728c3..9e65e900440 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -35,6 +35,7 @@ describe('Board List Header Component', () => {
withLocalStorage = true,
currentUserId = 1,
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
+ injectedProps = {},
} = {}) => {
const boardId = '1';
@@ -76,6 +77,7 @@ describe('Board List Header Component', () => {
currentUserId,
isEpicBoard: false,
disabled: false,
+ ...injectedProps,
},
}),
);
@@ -86,6 +88,7 @@ describe('Board List Header Component', () => {
const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' });
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
+ const findSettingsButton = () => wrapper.findComponent({ ref: 'settingsBtn' });
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
@@ -126,13 +129,40 @@ describe('Board List Header Component', () => {
});
});
+ describe('Settings Button', () => {
+ describe('with disabled=true', () => {
+ const hasSettings = [
+ ListType.assignee,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.label,
+ ];
+ const hasNoSettings = [ListType.backlog, ListType.closed];
+
+ it.each(hasSettings)('does render for List Type `%s` when disabled=true', (listType) => {
+ createComponent({ listType, injectedProps: { disabled: true } });
+
+ expect(findSettingsButton().exists()).toBe(true);
+ });
+
+ it.each(hasNoSettings)(
+ 'does not render for List Type `%s` when disabled=true',
+ (listType) => {
+ createComponent({ listType });
+
+ expect(findSettingsButton().exists()).toBe(false);
+ },
+ );
+ });
+ });
+
describe('expanding / collapsing the column', () => {
it('should display collapse icon when column is expanded', async () => {
createComponent();
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-down');
+ expect(icon.props('icon')).toBe('chevron-lg-down');
});
it('should display expand icon when column is collapsed', async () => {
@@ -140,7 +170,7 @@ describe('Board List Header Component', () => {
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-right');
+ expect(icon.props('icon')).toBe('chevron-lg-right');
});
it('should dispatch toggleListCollapse when clicking the collapse icon', async () => {
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index af492145eb0..8258d9fe7f4 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@@ -9,11 +11,18 @@ import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
+import { BoardType } from '~/boards/constants';
+
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import { mockProjectBoardResponse, mockGroupBoardResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+Vue.use(Vuex);
describe('BoardTopBar', () => {
let wrapper;
-
- Vue.use(Vuex);
+ let mockApollo;
const createStore = () => {
return new Vuex.Store({
@@ -21,10 +30,22 @@ describe('BoardTopBar', () => {
});
};
+ const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+ const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
+
const createComponent = ({ provide = {} } = {}) => {
const store = createStore();
+ mockApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+ ]);
+
wrapper = shallowMount(BoardTopBar, {
store,
+ apolloProvider: mockApollo,
+ props: {
+ boardId: 'gid://gitlab/Board/1',
+ },
provide: {
swimlanesFeatureAvailable: false,
canAdminList: false,
@@ -33,7 +54,9 @@ describe('BoardTopBar', () => {
boardType: 'group',
releasesFetchPath: '/releases',
isIssueBoard: true,
+ isEpicBoard: false,
isGroupBoard: true,
+ isApolloBoard: false,
...provide,
},
stubs: { IssueBoardFilteredSearch },
@@ -42,6 +65,7 @@ describe('BoardTopBar', () => {
afterEach(() => {
wrapper.destroy();
+ mockApollo = null;
});
describe('base template', () => {
@@ -83,4 +107,26 @@ describe('BoardTopBar', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(true);
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
+ createComponent({
+ provide: {
+ boardType,
+ isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === BoardType.group,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 7b61ca5e6fd..28f51e0ecbf 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -82,6 +82,7 @@ describe('BoardsSelector', () => {
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
isGroupBoard = false,
isProjectBoard = false,
+ provide = {},
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardsQuery, projectBoardsQueryHandler],
@@ -108,6 +109,8 @@ describe('BoardsSelector', () => {
boardType: isGroupBoard ? 'group' : 'project',
isGroupBoard,
isProjectBoard,
+ isApolloBoard: false,
+ ...provide,
},
});
};
@@ -245,4 +248,34 @@ describe('BoardsSelector', () => {
expect(notCalledHandler).not.toHaveBeenCalled();
});
});
+
+ describe('dropdown visibility', () => {
+ describe('when multipleIssueBoardsAvailable is enabled', () => {
+ it('show dropdown', async () => {
+ createStore();
+ createComponent({ provide: { multipleIssueBoardsAvailable: true } });
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when multipleIssueBoardsAvailable is disabled but it hasMissingBoards', () => {
+ it('show dropdown', async () => {
+ createStore();
+ createComponent({
+ provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: true },
+ });
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe("when multipleIssueBoardsAvailable is disabled and it dosn't hasMissingBoards", () => {
+ it('hide dropdown', async () => {
+ createStore();
+ createComponent({
+ provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: false },
+ });
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index cc1e5de15c1..bc66a0515aa 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
+import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
@@ -11,12 +11,14 @@ const TEST_ISSUE_A = {
iid: 8,
title: 'Issue 1',
referencePath: 'h/b#1',
+ webUrl: 'webUrl',
};
const TEST_ISSUE_B = {
id: 'gid://gitlab/Issue/2',
iid: 9,
title: 'Issue 2',
referencePath: 'h/b#2',
+ webUrl: 'webUrl',
};
describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
@@ -49,6 +51,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findAlert = () => wrapper.findComponent(GlAlert);
const findFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlLink = () => wrapper.findComponent(GlLink);
const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findTitle = () => wrapper.find('[data-testid="item-title"]');
@@ -67,6 +70,12 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
expect(findAlert().exists()).toBe(false);
});
+ it('links title to the corresponding issue', () => {
+ createWrapper();
+
+ expect(findGlLink().attributes('href')).toBe('webUrl');
+ });
+
describe('when new title is submitted', () => {
beforeEach(async () => {
createWrapper();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index df41eb05eae..1d011eacf1c 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,6 +1,7 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
OPERATORS_IS,
OPERATORS_IS_NOT,
@@ -50,6 +51,26 @@ export const mockBoard = {
weight: 2,
};
+export const mockProjectBoardResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/114',
+ board: mockBoard,
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupBoardResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/114',
+ board: mockBoard,
+ __typename: 'Group',
+ },
+ },
+};
+
export const mockBoardConfig = {
milestoneId: 'gid://gitlab/Milestone/114',
milestoneTitle: '14.9',
@@ -440,7 +461,7 @@ export const BoardsMockData = {
export const boardsMockInterceptor = (config) => {
const body = BoardsMockData[config.method.toUpperCase()][config.url];
- return [200, body];
+ return [HTTP_STATUS_OK, body];
};
export const mockList = {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index b3e90e34161..ab959abaa99 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -6,7 +6,6 @@ import {
inactiveId,
ISSUABLE,
ListType,
- issuableTypes,
BoardType,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
@@ -27,6 +26,7 @@ import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ISSUE } from '~/issues/constants';
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
@@ -167,7 +167,7 @@ describe('setFilters', () => {
])('should commit mutation SET_FILTERS %s', (_, { filters, filterVariables }) => {
const state = {
filters: {},
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
testAction(
@@ -299,9 +299,9 @@ describe('fetchLists', () => {
});
it.each`
- issuableType | boardType | fullBoardId | isGroup | isProject
- ${issuableTypes.issue} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
- ${issuableTypes.issue} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
+ issuableType | boardType | fullBoardId | isGroup | isProject
+ ${TYPE_ISSUE} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
+ ${TYPE_ISSUE} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
`(
'calls $issuableType query with correct variables',
async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => {
@@ -719,7 +719,7 @@ describe('updateList', () => {
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
boardItemsByListId,
});
@@ -835,7 +835,7 @@ describe('removeList', () => {
beforeEach(() => {
state = {
boardLists: mockListsById,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
getters = {
getListByTitle: jest.fn().mockReturnValue(mockList),
@@ -1747,7 +1747,7 @@ describe('setActiveItemSubscribed', () => {
[mockActiveIssue.id]: mockActiveIssue,
},
fullPath: 'gitlab-org',
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
const subscribedState = true;
@@ -1800,7 +1800,7 @@ describe('setActiveItemSubscribed', () => {
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
fullPath: 'path/f',
};
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 87a183c0441..2d68c070b83 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,8 +1,8 @@
import { cloneDeep } from 'lodash';
-import { issuableTypes } from '~/boards/constants';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import defaultState from '~/boards/stores/state';
+import { TYPE_ISSUE } from '~/issues/constants';
import {
mockBoard,
mockLists,
@@ -70,7 +70,7 @@ describe('Board Store Mutations', () => {
const fullPath = 'gitlab-org';
const boardType = 'group';
const disabled = false;
- const issuableType = issuableTypes.issue;
+ const issuableType = TYPE_ISSUE;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
allowSubEpics,
diff --git a/spec/frontend/branches/components/sort_dropdown_spec.js b/spec/frontend/branches/components/sort_dropdown_spec.js
index 16ed02bfa88..bd41b0daaaa 100644
--- a/spec/frontend/branches/components/sort_dropdown_spec.js
+++ b/spec/frontend/branches/components/sort_dropdown_spec.js
@@ -18,6 +18,8 @@ describe('Branches Sort Dropdown', () => {
updated_asc: 'Oldest updated',
updated_desc: 'Last updated',
},
+ showDropdown: false,
+ sortedBy: 'updated_desc',
...props,
},
}),
@@ -54,7 +56,7 @@ describe('Branches Sort Dropdown', () => {
describe('when in All branches mode', () => {
beforeEach(() => {
- wrapper = createWrapper({ mode: 'all' });
+ wrapper = createWrapper({ mode: 'all', showDropdown: true });
});
it('should have a search box with a placeholder', () => {
@@ -64,7 +66,7 @@ describe('Branches Sort Dropdown', () => {
expect(searchBox.find('input').attributes('placeholder')).toBe('Filter by branch name');
});
- it('should have a branches dropdown when in all branches mode', () => {
+ it('should have a branches dropdown', () => {
const branchesDropdown = findBranchesDropdown();
expect(branchesDropdown.exists()).toBe(true);
@@ -84,7 +86,7 @@ describe('Branches Sort Dropdown', () => {
searchBox.vm.$emit('submit');
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
- '/root/ci-cd-project-demo/-/branches?state=all',
+ '/root/ci-cd-project-demo/-/branches?state=all&sort=updated_desc',
);
});
});
diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js
index 7c367f83add..c3a0f6436c5 100644
--- a/spec/frontend/branches/divergence_graph_spec.js
+++ b/spec/frontend/branches/divergence_graph_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import init from '~/branches/divergence_graph';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Divergence graph', () => {
let mock;
@@ -8,7 +9,7 @@ describe('Divergence graph', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet('/-/diverging_counts').reply(200, {
+ mock.onGet('/-/diverging_counts').reply(HTTP_STATUS_OK, {
main: { ahead: 1, behind: 1 },
'test/hello-world': { ahead: 1, behind: 1 },
});
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index 002fe7c6e71..a4eecabcf28 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -34,8 +34,8 @@ describe('registerCaptchaModalInterceptor', () => {
waitForCaptchaToBeSolved.mockRejectedValue(new UnsolvedCaptchaError());
mock = new MockAdapter(axios);
- mock.onAny('/endpoint-without-captcha').reply(200, AXIOS_RESPONSE);
- mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-without-captcha').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-with-unrelated-error').reply(HTTP_STATUS_NOT_FOUND, AXIOS_RESPONSE);
mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }];
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index 3f1eebbc6a5..c0fb133b9b1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_GROUP_TYPE } from '~/ci/ci_variable_list/constants';
-
const mockProvide = {
glFeatures: {
groupScopedCiVariables: false,
@@ -36,7 +35,7 @@ describe('Ci Group Variable wrapper', () => {
it('are passed down the correctly to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
+ id: convertToGraphQLId(TYPENAME_GROUP, mockProvide.groupId),
areScopedVariablesAvailable: false,
componentName: 'GroupVariables',
entity: 'group',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index 7230017c560..bd1e6b17d6b 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_PROJECT_TYPE } from '~/ci/ci_variable_list/constants';
-
const mockProvide = {
projectFullPath: '/namespace/project',
projectId: 1,
@@ -32,7 +31,7 @@ describe('Ci Project Variable wrapper', () => {
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
+ id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
areScopedVariablesAvailable: true,
componentName: 'ProjectVariables',
entity: 'project',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 7838e4884d8..508af964ca3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -21,6 +21,8 @@ describe('Ci variable modal', () => {
let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+ const maskableRawRegex = '^\\S{8,}$';
+
const mockVariables = mockVariablesWithScopes(instanceString);
const defaultProvide = {
@@ -30,10 +32,13 @@ describe('Ci variable modal', () => {
awsTipLearnLink: '/learn-link',
containsVariableReferenceLink: '/reference',
environmentScopeLink: '/help/environments',
+ glFeatures: {
+ ciRemoveCharacterLimitationRawMaskedVar: true,
+ },
isProtectedByDefault: false,
maskedEnvironmentVariablesLink: '/variables-link',
+ maskableRawRegex,
maskableRegex,
- protectedEnvironmentVariablesLink: '/protected-link',
};
const defaultProps = {
@@ -424,6 +429,36 @@ describe('Ci variable modal', () => {
describe('Validations', () => {
const maskError = 'This variable can not be masked.';
+ describe('when the variable is raw', () => {
+ const [variable] = mockVariables;
+ const validRawMaskedVariable = {
+ ...variable,
+ value: 'd$%^asdsadas',
+ masked: false,
+ raw: true,
+ };
+
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: validRawMaskedVariable },
+ });
+ });
+
+ it('should not show an error with symbols', async () => {
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).not.toContain(maskError);
+ });
+
+ it('should not show an error when length is less than 8', async () => {
+ await findValueField().vm.$emit('input', 'a');
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).toContain(maskError);
+ });
+ });
+
describe('when the mask state is invalid', () => {
beforeEach(async () => {
const [variable] = mockVariables;
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 2d39bff8ce0..c977ae773db 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
@@ -227,7 +228,7 @@ describe('Ci Variable Shared Component', () => {
variables: {
endpoint: mockProvide.endpoint,
fullPath: groupProps.fullPath,
- id: convertToGraphQLId('Group', groupProps.id),
+ id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
variable: newVariable,
},
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index d7f0ce838d6..dc72694d26f 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -11,11 +11,12 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = ({ showDrawer = false } = {}) => {
+ const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => {
wrapper = extendedWrapper(
shallowMount(CiEditorHeader, {
propsData: {
showDrawer,
+ showJobAssistantDrawer,
},
}),
);
diff --git a/spec/frontend/ci/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 6f28362e478..7bf955012c7 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -47,11 +47,6 @@ describe('Pipeline Status', () => {
mockLinkedPipelinesQuery = jest.fn();
});
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
describe('when there are stages', () => {
beforeEach(() => {
createComponent();
@@ -74,9 +69,11 @@ describe('Pipeline Status', () => {
describe('when querying upstream and downstream pipelines', () => {
describe('when query succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
createComponentWithApollo();
+
+ await waitForPromises();
});
it('should call the query with the correct variables', () => {
@@ -86,6 +83,10 @@ describe('Pipeline Status', () => {
iid: mockProjectPipeline().pipeline.iid,
});
});
+
+ it('renders only the latest downstream pipelines', () => {
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
});
describe('when query fails', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
deleted file mode 100644
index 6f28362e478..00000000000
--- a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-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 '~/ci/pipeline_editor/constants';
-import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
-
-Vue.use(VueApollo);
-
-describe('Pipeline Status', () => {
- let wrapper;
- let mockApollo;
- let mockLinkedPipelinesQuery;
-
- const createComponent = ({ hasStages = true, options } = {}) => {
- wrapper = shallowMount(PipelineEditorMiniGraph, {
- provide: {
- dataMethod: 'graphql',
- projectFullPath: mockProjectFullPath,
- },
- propsData: {
- pipeline: mockProjectPipeline({ hasStages }).pipeline,
- },
- ...options,
- });
- };
-
- const createComponentWithApollo = (hasStages = true) => {
- const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
- mockApollo = createMockApollo(handlers);
-
- createComponent({
- hasStages,
- options: {
- apolloProvider: mockApollo,
- },
- });
- };
-
- const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
-
- beforeEach(() => {
- mockLinkedPipelinesQuery = jest.fn();
- });
-
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
- describe('when there are stages', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(true);
- });
- });
-
- describe('when there are no stages', () => {
- beforeEach(() => {
- createComponent({ hasStages: false });
- });
-
- it('does not render pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(false);
- });
- });
-
- describe('when querying upstream and downstream pipelines', () => {
- describe('when query succeeds', () => {
- beforeEach(() => {
- mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
- createComponentWithApollo();
- });
-
- it('should call the query with the correct variables', () => {
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
- fullPath: mockProjectFullPath,
- iid: mockProjectPipeline().pipeline.iid,
- });
- });
- });
-
- describe('when query fails', () => {
- beforeEach(async () => {
- mockLinkedPipelinesQuery.mockRejectedValue(new Error());
- createComponentWithApollo();
- await waitForPromises();
- });
-
- it('should emit an error event when query fails', async () => {
- expect(wrapper.emitted('showError')).toHaveLength(1);
- expect(wrapper.emitted('showError')[0]).toEqual([
- {
- type: PIPELINE_FAILURE,
- reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
- },
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
new file mode 100644
index 00000000000..79200d92598
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -0,0 +1,45 @@
+import { GlDrawer } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+Vue.use(VueApollo);
+
+describe('Job assistant drawer', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+
+ const createComponent = () => {
+ wrapper = mountExtended(JobAssistantDrawer, {
+ propsData: {
+ isVisible: true,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should emit close job assistant drawer event when closing the drawer', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+
+ it('should emit close job assistant drawer event when click cancel button', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findCancelButton().trigger('click');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 70310cbdb10..f40db50aab7 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -56,6 +56,7 @@ describe('Pipeline editor tabs component', () => {
currentTab: CREATE_TAB,
isNewCiConfigFile: true,
showDrawer: false,
+ showJobAssistantDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 176dc24f169..541123d7efc 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -373,13 +373,64 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
- project: { name: 'job-log-sections', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: false,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-611-611',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: true,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: true,
+ },
__typename: 'Pipeline',
},
],
@@ -391,8 +442,13 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
- project: { name: 'trigger-downstream', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-610-610',
group: 'success',
icon: 'status_success',
label: 'passed',
@@ -405,7 +461,9 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
return {
data: {
project: {
+ id: 'gid://gitlab/Project/21',
pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/790',
path: '/root/ci-project/-/pipelines/790',
downstream,
upstream,
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index 2246d0bbf7e..a103acb33bc 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
@@ -343,7 +344,7 @@ describe('Pipeline editor app component', () => {
describe('when the lint query returns a 500 error', () => {
beforeEach(async () => {
- mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ mockCiConfigData.mockRejectedValueOnce(new Error(HTTP_STATUS_INTERNAL_SERVER_ERROR));
await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 621e015e825..4f8f2112abe 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -6,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
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 JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_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';
@@ -56,11 +57,13 @@ describe('Pipeline editor home wrapper', () => {
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
+ const findJobAssistantDrawer = () => wrapper.findComponent(JobAssistantDrawer);
const findPipelineEditorFileTree = () => wrapper.findComponent(PipelineEditorFileTree);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
+ const findJobAssistantBtn = () => wrapper.findByTestId('job-assistant-drawer-toggle');
afterEach(() => {
localStorage.clear();
@@ -261,6 +264,110 @@ describe('Pipeline editor home wrapper', () => {
});
});
+ describe('job assistant drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+ const clickJobAssistantBtn = async () => {
+ findJobAssistantBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ const stubs = {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ JobAssistantDrawer,
+ };
+
+ it('hides the job assistant drawer by default', () => {
+ createComponent({
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the job assistant drawer on button click', async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the job assistant drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ findJobAssistantDrawer().findComponent(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('covers helper drawer when opened last', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickHelpBtn();
+ await clickJobAssistantBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeGreaterThan(pipelineEditorDrawerIndex);
+ });
+
+ it('covered by helper drawer when opened first', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+ await clickHelpBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeLessThan(pipelineEditorDrawerIndex);
+ });
+ });
+
describe('file tree', () => {
const toggleFileTree = async () => {
findFileTreeBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index cd16045f92d..6f18899ebac 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -14,7 +14,9 @@ import {
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
-import PipelineNewForm from '~/ci/pipeline_new/components/pipeline_new_form.vue';
+import PipelineNewForm, {
+ POLLING_INTERVAL,
+} from '~/ci/pipeline_new/components/pipeline_new_form.vue';
import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
import { resolvers } from '~/ci/pipeline_new/graphql/resolvers';
import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
@@ -24,6 +26,7 @@ import {
mockCiConfigVariablesResponseWithoutDesc,
mockEmptyCiConfigVariablesResponse,
mockError,
+ mockNoCachedCiConfigVariablesResponse,
mockQueryParams,
mockPostParams,
mockProjectId,
@@ -69,6 +72,10 @@ describe('Pipeline New Form', () => {
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
+ const advanceToNextFetch = (milliseconds) => {
+ jest.advanceTimersByTime(milliseconds);
+ };
+
const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
@@ -266,17 +273,98 @@ describe('Pipeline New Form', () => {
});
});
- describe('when yml defines a variable', () => {
- it('loading icon is shown when content is requested and hidden when received', async () => {
- mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ describe('When there are no variables in the API cache', () => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockNoCachedCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+ });
+ it('stops polling after CONFIG_VARIABLES_TIMEOUT ms have passed', async () => {
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+ });
+
+ it('shows loading icon while query polls for updated values', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+ });
+
+ it('hides loading icon and stops polling after query fetches the updated values', async () => {
expect(findLoadingIcon().exists()).toBe(true);
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ advanceToNextFetch(POLLING_INTERVAL);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
+ });
+
+ const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(queryResponse);
+ createComponentWithApollo({ method: mountExtended });
+ });
+
+ it('does not poll for new values', async () => {
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+ });
+
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ };
+
+ describe('When no variables are defined in the CI configuration and the cache is updated', () => {
+ testBehaviorWhenCacheIsPopulated(mockEmptyCiConfigVariablesResponse);
+
+ it('displays an empty form', async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+
+ expect(findKeyInputs().at(0).element.value).toBe('');
+ expect(findValueInputs().at(0).element.value).toBe('');
+ expect(findVariableTypes().at(0).props('text')).toBe('Variable');
+ });
+ });
+
+ describe('When CI configuration has defined variables and they are stored in the cache', () => {
+ testBehaviorWhenCacheIsPopulated(mockCiConfigVariablesResponse);
describe('with different predefined values', () => {
beforeEach(async () => {
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index dfb643a0ba4..5b935c0c819 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -132,3 +132,4 @@ export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResp
export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
mockYamlVariablesWithoutDesc,
);
+export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 5ca4b25da9b..90ca2a07266 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -38,20 +38,20 @@ describe('code quality issue body issue body', () => {
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
- ${'INFO'} | ${'text-primary-400'} | ${'severity-info'}
- ${'MINOR'} | ${'text-warning-200'} | ${'severity-low'}
- ${'CRITICAL'} | ${'text-danger-600'} | ${'severity-high'}
- ${'BLOCKER'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'UNKNOWN'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'INVALID'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'info'} | ${'text-primary-400'} | ${'severity-info'}
- ${'minor'} | ${'text-warning-200'} | ${'severity-low'}
- ${'major'} | ${'text-warning-400'} | ${'severity-medium'}
- ${'critical'} | ${'text-danger-600'} | ${'severity-high'}
- ${'blocker'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${undefined} | ${'text-secondary-400'} | ${'severity-unknown'}
+ ${'INFO'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'MINOR'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'CRITICAL'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'BLOCKER'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'UNKNOWN'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'INVALID'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'info'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'minor'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'major'} | ${'gl-text-orange-400'} | ${'severity-medium'}
+ ${'critical'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'blocker'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'unknown'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'invalid'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${undefined} | ${'gl-text-gray-400'} | ${'severity-unknown'}
`(
'renders correct icon for "$severity" severity rating',
({ severity, iconClass, iconName }) => {
diff --git a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
index 88628210793..a606bce3d78 100644
--- a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
@@ -2,6 +2,11 @@ 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 {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
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';
@@ -55,7 +60,7 @@ describe('Codequality Reports actions', () => {
describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', () => {
- mock.onGet(endpoint).reply(200, reportIssues);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, reportIssues);
return testAction(
actions.fetchReports,
@@ -74,7 +79,7 @@ describe('Codequality Reports actions', () => {
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
- mock.onGet(endpoint).reply(500);
+ mock.onGet(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.fetchReports,
@@ -89,7 +94,7 @@ describe('Codequality Reports actions', () => {
describe('when base report is not found', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
const data = { status: STATUS_NOT_FOUND };
- mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data);
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(HTTP_STATUS_OK, data);
return testAction(
actions.fetchReports,
@@ -105,9 +110,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives data', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(200, reportIssues);
+ .reply(HTTP_STATUS_OK, reportIssues);
return Promise.all([
testAction(
@@ -134,9 +139,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives an error', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(500);
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Promise.all([
testAction(
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
new file mode 100644
index 00000000000..edf3d1706cc
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+
+const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
+
+Vue.use(VueApollo);
+
+describe('AdminNewRunnerApp', () => {
+ let wrapper;
+
+ const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(AdminNewRunnerApp, {
+ propsData: {
+ legacyRegistrationToken: mockLegacyRegistrationToken,
+ ...props,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ stubs: {
+ GlSprintf,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Shows legacy modal', () => {
+ it('passes legacy registration to modal', () => {
+ expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
+ mockLegacyRegistrationToken,
+ );
+ });
+
+ it('opens a modal with the legacy instructions', () => {
+ const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+
+ expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ });
+ });
+
+ describe('New runner form fields', () => {
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner', () => {
+ it('shows the runners fields', () => {
+ expect(findRunnerFormFields().props('value')).toEqual({
+ accessLevel: 'NOT_PROTECTED',
+ paused: false,
+ description: '',
+ maintenanceNote: '',
+ maximumTimeout: ' ',
+ runUntagged: false,
+ tagList: '',
+ });
+ });
+ });
+ });
+});
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 e233268b756..ed4f43c12d8 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,8 +1,6 @@
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';
@@ -15,6 +13,7 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
@@ -42,14 +41,12 @@ describe('AdminRunnerShowApp', () => {
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);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
- const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -89,16 +86,20 @@ describe('AdminRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the runner header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
it('displays the runner edit and pause buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
+ });
+
it('shows basic runner details', async () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -118,20 +119,11 @@ 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({
userPermissions: {
+ ...mockRunner.userPermissions,
updateRunner: false,
},
});
@@ -145,12 +137,17 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
+
+ it('displays delete button', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(true);
+ });
});
describe('when runner cannot be deleted', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
+ ...mockRunner.userPermissions,
deleteRunner: false,
},
});
@@ -160,9 +157,14 @@ describe('AdminRunnerShowApp', () => {
});
});
- it('does not display the runner edit and pause buttons', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
+
+ it('displays edit and pause buttons', () => {
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
});
describe('when runner is deleted', () => {
@@ -240,74 +242,4 @@ describe('AdminRunnerShowApp', () => {
expect(createAlert).toHaveBeenCalled();
});
});
-
- describe('When showing jobs', () => {
- const stubs = {
- GlTab,
- GlTabs,
- };
-
- it('without a runner, shows no jobs', () => {
- mockRunnerQuery = jest.fn().mockResolvedValue({
- data: {
- runner: null,
- },
- });
-
- createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- 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 });
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- });
-
- it('with a job count, shows jobs count', async () => {
- const runner = { jobCount: 3 };
- mockRunnerQueryResult(runner);
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().text()).toBe('3');
- });
- });
-
- 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 9084ecdb4cc..7fc240e520b 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
@@ -39,6 +39,7 @@ import {
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
INSTANCE_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -56,6 +57,7 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ newRunnerPath,
emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
@@ -113,6 +115,7 @@ describe('AdminRunnersApp', () => {
apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
provide: {
@@ -280,11 +283,14 @@ describe('AdminRunnersApp', () => {
it('Shows job status and links to jobs', () => {
const badge = wrapper
- .find('tr [data-testid="td-summary"]')
+ .find('tr [data-testid="td-status"]')
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
- expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+
+ const badgeHref = new URL(badge.attributes('href'));
+ expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
+ expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
});
it('When runner is paused or unpaused, some data is refetched', async () => {
@@ -443,7 +449,13 @@ describe('AdminRunnersApp', () => {
});
it('shows an empty state', () => {
- expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
+ expect(findRunnerListEmptyState().props()).toEqual({
+ newRunnerPath,
+ isSearchFiltered: false,
+ filteredSvgPath: emptyStateFilteredSvgPath,
+ registrationToken: mockRegistrationToken,
+ svgPath: emptyStateSvgPath,
+ });
});
describe('when a filter is selected by the user', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 2fb824a8fa5..1ff60ff1a9d 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -10,6 +10,7 @@ import {
INSTANCE_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
+ JOB_STATUS_IDLE,
} from '~/ci/runner/constants';
describe('RunnerStatusCell', () => {
@@ -18,16 +19,18 @@ describe('RunnerStatusCell', () => {
const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
- const createComponent = ({ runner = {} } = {}) => {
+ const createComponent = ({ runner = {}, ...options } = {}) => {
wrapper = mount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
active: true,
status: STATUS_ONLINE,
+ jobExecutionStatus: JOB_STATUS_IDLE,
...runner,
},
},
+ ...options,
});
};
@@ -74,4 +77,14 @@ describe('RunnerStatusCell', () => {
expect(wrapper.text()).toBe('');
});
+
+ it('Displays "runner-job-status-badge" slot', () => {
+ createComponent({
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Job status ${JOB_STATUS_IDLE}`);
+ });
});
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 10280c77303..1711df42491 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -3,7 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
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';
@@ -22,7 +21,6 @@ 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)
@@ -95,10 +93,6 @@ describe('RunnerTypeCell', () => {
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',
@@ -166,14 +160,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
+ it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- [slotName]: slotContent,
+ 'runner-name': slotContent,
},
},
);
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0ecafdd7d83..0daaca9c4ff 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,8 +1,9 @@
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-
import VueApollo from 'vue-apollo';
+
+import { s__ } from '~/locale';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -84,9 +85,9 @@ describe('RegistrationDropdown', () => {
it.each`
type | text
- ${INSTANCE_TYPE} | ${'Register an instance runner'}
- ${GROUP_TYPE} | ${'Register a group runner'}
- ${PROJECT_TYPE} | ${'Register a project runner'}
+ ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
+ ${GROUP_TYPE} | ${s__('Runners|Register a group runner')}
+ ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')}
`('Dropdown text for type $type is "$text"', () => {
createComponent({ props: { type: INSTANCE_TYPE } }, mount);
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 64f5a0e3b57..0dc5a90fb83 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { createAlert } from '~/flash';
@@ -22,6 +23,7 @@ describe('RunnerBulkDelete', () => {
let mockState;
let mockCheckedRunnerIds;
+ const findBanner = () => wrapper.findByTestId('runner-bulk-delete-banner');
const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection'));
const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected'));
const findModal = () => wrapper.findComponent(GlModal);
@@ -64,10 +66,11 @@ describe('RunnerBulkDelete', () => {
beforeEach(() => {
mockState = createLocalState();
+ mockCheckedRunnerIds = makeVar([]);
jest
.spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
- .mockImplementation(() => mockCheckedRunnerIds);
+ .mockImplementation(() => mockCheckedRunnerIds());
});
afterEach(() => {
@@ -76,15 +79,13 @@ describe('RunnerBulkDelete', () => {
describe('When no runners are checked', () => {
beforeEach(async () => {
- mockCheckedRunnerIds = [];
-
createComponent();
await waitForPromises();
});
it('shows no contents', () => {
- expect(wrapper.html()).toBe('');
+ expect(findBanner().exists()).toBe(false);
});
});
@@ -94,7 +95,7 @@ describe('RunnerBulkDelete', () => {
${2} | ${[mockId1, mockId2]} | ${'2 runners'}
`('When $count runner(s) are checked', ({ ids, text }) => {
beforeEach(() => {
- mockCheckedRunnerIds = ids;
+ mockCheckedRunnerIds(ids);
createComponent();
@@ -102,7 +103,7 @@ describe('RunnerBulkDelete', () => {
});
it(`shows "${text}"`, () => {
- expect(wrapper.text()).toContain(text);
+ expect(findBanner().text()).toContain(text);
});
it('clears selection', () => {
@@ -133,7 +134,7 @@ describe('RunnerBulkDelete', () => {
};
beforeEach(() => {
- mockCheckedRunnerIds = [mockId1, mockId2];
+ mockCheckedRunnerIds([mockId1, mockId2]);
createComponent();
@@ -157,20 +158,23 @@ describe('RunnerBulkDelete', () => {
it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
- input: { ids: mockCheckedRunnerIds },
+ input: { ids: mockCheckedRunnerIds() },
});
});
});
describe('when deletion is successful', () => {
+ let deletedIds;
+
beforeEach(async () => {
+ deletedIds = mockCheckedRunnerIds();
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
- bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ bulkRunnerDelete: { deletedIds, errors: [] },
},
});
-
confirmDeletion();
+ mockCheckedRunnerIds([]);
await waitForPromises();
});
@@ -182,12 +186,12 @@ describe('RunnerBulkDelete', () => {
it('user interface is updated', () => {
const { evict, gc } = apolloCache;
- expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
+ expect(evict).toHaveBeenCalledTimes(deletedIds.length);
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[0]),
+ id: expect.stringContaining(deletedIds[0]),
});
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[1]),
+ id: expect.stringContaining(deletedIds[1]),
});
expect(gc).toHaveBeenCalledTimes(1);
@@ -195,7 +199,7 @@ describe('RunnerBulkDelete', () => {
it('emits deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toEqual([
- [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
+ [{ message: expect.stringContaining(`${deletedIds.length}`) }],
]);
});
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
new file mode 100644
index 00000000000..a59c5a21377
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -0,0 +1,127 @@
+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 waitForPromises from 'helpers/wait_for_promises';
+import { JOBS_ROUTE_PATH, I18N_DETAILS, I18N_JOBS } from '~/ci/runner/constants';
+
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
+
+import { runnerData } from '../mock_data';
+
+// Vue Test Utils `stubs` option does not stub components mounted
+// in <router-view>. Use mocking instead:
+jest.mock('~/ci/runner/components/runner_jobs.vue', () => {
+ const ActualRunnerJobs = jest.requireActual('~/ci/runner/components/runner_jobs.vue').default;
+ return {
+ props: ActualRunnerJobs.props,
+ render() {},
+ };
+});
+
+const mockRunner = runnerData.data.runner;
+
+Vue.use(VueApollo);
+Vue.use(VueRouter);
+
+describe('RunnerDetailsTabs', () => {
+ let wrapper;
+ let routerPush;
+
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
+ const findRunnerJobs = () => wrapper.findComponent(RunnerJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerDetailsTabs, {
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ ...options,
+ });
+
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+
+ return waitForPromises();
+ };
+
+ it('shows basic runner details', async () => {
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().props('runner')).toBe(mockRunner);
+ expect(findRunnerJobs().exists()).toBe(false);
+ });
+
+ it('shows runner jobs', async () => {
+ setWindowLocation(`#${JOBS_ROUTE_PATH}`);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().exists()).toBe(false);
+ expect(findRunnerJobs().props('runner')).toBe(mockRunner);
+ });
+
+ it.each`
+ jobCount | badgeText
+ ${null} | ${null}
+ ${1} | ${'1'}
+ ${1000} | ${'1,000'}
+ ${1001} | ${'1,000+'}
+ `('shows runner jobs count', async ({ jobCount, badgeText }) => {
+ await createComponent({
+ stubs: {
+ GlTab,
+ },
+ props: {
+ runner: {
+ ...mockRunner,
+ jobCount,
+ },
+ },
+ });
+
+ if (!badgeText) {
+ expect(findJobCountBadge().exists()).toBe(false);
+ } else {
+ expect(findJobCountBadge().text()).toBe(badgeText);
+ }
+ });
+
+ 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(findRunnerJobs().exists()).toBe(false);
+ });
+
+ describe.each`
+ location | tab | navigatedTo
+ ${'#/details'} | ${I18N_DETAILS} | ${[]}
+ ${'#/details'} | ${I18N_JOBS} | ${[[{ name: 'jobs' }]]}
+ ${'#/jobs'} | ${I18N_JOBS} | ${[]}
+ ${'#/jobs'} | ${I18N_DETAILS} | ${[[{ name: 'details' }]]}
+ `('When at $location', ({ location, tab, navigatedTo }) => {
+ beforeEach(async () => {
+ setWindowLocation(location);
+
+ await createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
+ it(`on click on ${tab}, navigates to ${JSON.stringify(navigatedTo)}`, () => {
+ wrapper.findByText(tab).trigger('click');
+
+ expect(routerPush.mock.calls).toEqual(navigatedTo);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
new file mode 100644
index 00000000000..5b429645d17
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -0,0 +1,87 @@
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '~/ci/runner/constants';
+
+const mockDescription = 'My description';
+const mockMaxTimeout = 60;
+const mockTags = 'tag, tag2';
+
+describe('RunnerFormFields', () => {
+ let wrapper;
+
+ const findInput = (name) => wrapper.find(`input[name="${name}"]`);
+
+ const createComponent = ({ runner } = {}) => {
+ wrapper = mountExtended(RunnerFormFields, {
+ propsData: {
+ value: runner,
+ },
+ });
+ };
+
+ it('updates runner fields', async () => {
+ createComponent();
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+
+ findInput('description').setValue(mockDescription);
+ findInput('max-timeout').setValue(mockMaxTimeout);
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+ findInput('tags').setValue(mockTags);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject({
+ description: mockDescription,
+ maximumTimeout: mockMaxTimeout,
+ tagList: mockTags,
+ });
+ });
+
+ it('checks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ },
+ });
+
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ });
+ });
+
+ it('unchecks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ },
+ });
+
+ findInput('paused').setChecked(false);
+ findInput('protected').setChecked(false);
+ findInput('run-untagged').setChecked(false);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index a04011de1cd..abe3b47767e 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -6,7 +6,7 @@ import {
GROUP_TYPE,
STATUS_ONLINE,
} from '~/ci/runner/constants';
-import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -71,7 +71,7 @@ describe('RunnerHeader', () => {
it('displays the runner id', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
},
});
@@ -99,7 +99,7 @@ describe('RunnerHeader', () => {
it('does not display runner creation time if "createdAt" is missing', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
createdAt: null,
},
});
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
index 015bebf40e3..c4476d01386 100644
--- a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -23,16 +23,25 @@ describe('RunnerTypeBadge', () => {
};
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}
+ jobStatus | classes | text
+ ${JOB_STATUS_RUNNING} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
+ ${JOB_STATUS_IDLE} | ${['gl-text-gray-700!', '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().props()).toMatchObject({ size: 'md', variant: 'muted' });
+ expect(findBadge().classes().sort()).toEqual(
+ [
+ ...classes,
+ 'gl-border',
+ 'gl-display-inline-block',
+ 'gl-max-w-full',
+ 'gl-text-truncate',
+ 'gl-bg-transparent!',
+ ].sort(),
+ );
expect(findBadge().text()).toBe(text);
},
);
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 8defe568df8..281aa1aeb77 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -72,7 +72,7 @@ describe('RunnerJobsTable', () => {
});
it('Displays details of a job', () => {
- const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
+ const { id, detailedStatus, project, shortSha, commitPath } = mockJobs[0];
expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
@@ -81,10 +81,8 @@ describe('RunnerJobsTable', () => {
detailedStatus.detailsPath,
);
- expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
- expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
- pipeline.project.webUrl,
- );
+ expect(findCell({ field: 'project' }).text()).toBe(project.name);
+ expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(project.webUrl);
expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index d351f7b6908..6aea3ddf58c 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -4,10 +4,14 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import {
+ newRunnerPath,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
+} from 'jest/ci/runner/mock_data';
+
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockSvgPath = 'mock-svg-path.svg';
-const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
const mockRegistrationToken = 'REGISTRATION_TOKEN';
describe('RunnerListEmptyState', () => {
@@ -17,12 +21,13 @@ describe('RunnerListEmptyState', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- svgPath: mockSvgPath,
- filteredSvgPath: mockFilteredSvgPath,
+ svgPath: emptyStateSvgPath,
+ filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
directives: {
@@ -33,6 +38,7 @@ describe('RunnerListEmptyState', () => {
GlSprintf,
GlLink,
},
+ ...options,
});
};
@@ -45,7 +51,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text with instructions', () => {
@@ -56,10 +62,53 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ describe('when create_runner_workflow is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
+ });
+
+ describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
+
+ describe('when create_runner_workflow is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: false },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
@@ -69,7 +118,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text', () => {
@@ -92,7 +141,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders a "filtered search" illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateFilteredSvgPath);
});
it('displays "no filtered results" text', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 1267d045623..2e5d1dbd063 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -177,30 +177,30 @@ describe('RunnerList', () => {
});
describe('Scoped cell slots', () => {
- it('Render #runner-name slot in "summary" cell', () => {
+ it('Render #runner-job-status-badge slot in "status" cell', () => {
createComponent(
{
- scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
+ expect(findCell({ fieldKey: 'status' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
});
- it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ it('Render #runner-name slot in "summary" cell', () => {
createComponent(
{
- scopedSlots: {
- 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
- },
+ scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(
- `Job status ${mockRunners[0].jobExecutionStatus}`,
- );
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
it('Render #runner-actions-cell slot in "actions" cell', () => {
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
new file mode 100644
index 00000000000..db6fd2c369b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -0,0 +1,96 @@
+import { nextTick } from 'vue';
+import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ AWS_PLATFORM,
+ DOCKER_HELP_URL,
+ KUBERNETES_HELP_URL,
+} from '~/ci/runner/constants';
+
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+
+const mockProvide = {
+ awsImgPath: 'awsLogo.svg',
+ dockerImgPath: 'dockerLogo.svg',
+ kubernetesImgPath: 'kubernetesLogo.svg',
+};
+
+describe('RunnerPlatformsRadioGroup', () => {
+ let wrapper;
+
+ const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findFormRadios = () => wrapper.findAllComponents(RunnerPlatformsRadio).wrappers;
+ const findFormRadioByText = (text) =>
+ findFormRadios()
+ .filter((w) => w.text() === text)
+ .at(0);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadioGroup, {
+ propsData: {
+ value: null,
+ ...props,
+ },
+ provide: mockProvide,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('contains expected options with images', () => {
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
+
+ expect(labels).toEqual([
+ ['Linux', null],
+ ['macOS', null],
+ ['Windows', null],
+ ['AWS', expect.any(String)],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
+ });
+
+ it('allows users to use radio group', async () => {
+ findFormRadioGroup().vm.$emit('input', MACOS_PLATFORM);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([MACOS_PLATFORM]);
+ });
+
+ it.each`
+ text | value
+ ${'Linux'} | ${LINUX_PLATFORM}
+ ${'macOS'} | ${MACOS_PLATFORM}
+ ${'Windows'} | ${WINDOWS_PLATFORM}
+ ${'AWS'} | ${AWS_PLATFORM}
+ `('user can select "$text"', async ({ text, value }) => {
+ const radio = findFormRadioByText(text);
+ expect(radio.props('value')).toBe(value);
+
+ radio.vm.$emit('input', value);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([value]);
+ });
+
+ it.each`
+ text | href
+ ${'Docker'} | ${DOCKER_HELP_URL}
+ ${'Kubernetes'} | ${KUBERNETES_HELP_URL}
+ `('provides link to "$text" docs', async ({ text, href }) => {
+ const radio = findFormRadioByText(text);
+
+ expect(radio.findComponent(GlLink).attributes()).toEqual({
+ href,
+ target: '_blank',
+ });
+ expect(radio.findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
new file mode 100644
index 00000000000..fb81edd1ae2
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -0,0 +1,154 @@
+import { GlFormRadio } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+
+const mockImg = 'mock.svg';
+const mockValue = 'value';
+const mockValue2 = 'value2';
+const mockSlot = '<div>a</div>';
+
+describe('RunnerPlatformsRadio', () => {
+ let wrapper;
+
+ const findDiv = () => wrapper.find('div');
+ const findImg = () => wrapper.find('img');
+ const findFormRadio = () => wrapper.findComponent(GlFormRadio);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadio, {
+ propsData: {
+ image: mockImg,
+ value: mockValue,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ describe('when its selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(true);
+ });
+
+ it('shows radio option', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ });
+
+ it('emits when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toEqual([[mockValue]]);
+ });
+
+ it.each(['input', 'change'])('emits radio "%s" event', (event) => {
+ findFormRadio().vm.$emit(event, mockValue2);
+
+ expect(wrapper.emitted(event)).toEqual([[mockValue2]]);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when its not selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
+ });
+
+ it('does not emit when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+ });
+
+ it('does not show a radio option', () => {
+ expect(findFormRadio().exists()).toBe(false);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when selected', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { checked: mockValue },
+ });
+ });
+
+ it('highlights the item', () => {
+ expect(wrapper.classes('gl-bg-blue-50')).toBe(true);
+ expect(wrapper.classes('gl-border-blue-500')).toBe(true);
+ });
+
+ it('shows radio option as selected', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ expect(findFormRadio().props('checked')).toBe(mockValue);
+ });
+ });
+});
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 3dce5a509ca..b7d9d3ad23e 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
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -80,10 +80,10 @@ describe('TagToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
+ mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockTags);
mock
.onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } })
- .reply(200, mockTagsFiltered);
+ .reply(HTTP_STATUS_OK, mockTagsFiltered);
getRecentlyUsedSuggestions.mockReturnValue([]);
});
@@ -163,7 +163,9 @@ describe('TagToken', () => {
describe('when suggestions cannot be loaded', () => {
beforeEach(async () => {
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500);
+ mock
+ .onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index c6c3f3b7040..2ad31dea774 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -12,6 +13,9 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
+
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
import GroupRunnerShowApp from '~/ci/runner/group_runner_show/group_runner_show_app.vue';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -31,6 +35,7 @@ const mockRunnersPath = '/groups/group1/-/runners';
const mockEditGroupRunnerPath = `/groups/group1/-/runners/${mockRunnerId}/edit`;
Vue.use(VueApollo);
+Vue.use(VueRouter);
describe('GroupRunnerShowApp', () => {
let wrapper;
@@ -41,6 +46,8 @@ describe('GroupRunnerShowApp', () => {
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -81,16 +88,23 @@ describe('GroupRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays edit, pause, delete buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ it('displays the runner edit and pause buttons', async () => {
+ expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props()).toEqual({
+ runner: mockRunner,
+ showAccessHelp: true,
+ });
+ });
+
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -104,17 +118,12 @@ describe('GroupRunnerShowApp', () => {
Token expiry
Runner authentication token expiration
Runner authentication tokens will expire based on a set interval.
- They will automatically rotate once expired. Learn more
- Never expires
+ They will automatically rotate once expired. Learn more Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- it('renders runner details component', () => {
- expect(findRunnerDetails().props('runner')).toEqual(mockRunner);
- });
-
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
@@ -129,7 +138,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display edit and pause buttons', () => {
+ it('does not display the runner edit and pause buttons', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
@@ -153,7 +162,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display delete button', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
@@ -187,8 +196,17 @@ describe('GroupRunnerShowApp', () => {
mockRunnerQueryResult();
createComponent();
+
expect(findRunnerDetails().exists()).toBe(false);
});
+
+ it('does not show runner jobs', () => {
+ mockRunnerQueryResult();
+
+ createComponent();
+
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
});
describe('When there is an error', () => {
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 1e5bb828dbf..39ea5cade28 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
@@ -25,6 +25,7 @@ import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
CREATED_ASC,
@@ -35,6 +36,7 @@ import {
I18N_STATUS_STALE,
INSTANCE_TYPE,
GROUP_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -112,7 +114,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -254,7 +255,7 @@ describe('GroupRunnersApp', () => {
let showToast;
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
- const { id: graphqlId, shortSha } = node;
+ const { id: graphqlId, shortSha, jobExecutionStatus } = node;
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 6; // Smart queries that display a count of runners in tabs and single stats
@@ -264,6 +265,13 @@ describe('GroupRunnersApp', () => {
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
});
+ it('Shows job status and links to jobs', () => {
+ const badge = findRunnerRow(id).findByTestId('td-status').findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`${webUrl}#${JOBS_ROUTE_PATH}`);
+ });
+
it('view link is displayed correctly', () => {
const viewLink = findRunnerRow(id).findByTestId('td-summary').findComponent(GlLink);
@@ -466,7 +474,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
@@ -482,7 +489,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: null,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 525756ed513..5cdf0ea4e3b 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -304,6 +304,7 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const newRunnerPath = '/runners/new';
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
new file mode 100644
index 00000000000..b2084e3a7de
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
@@ -0,0 +1,386 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the snapshot 1`] = `
+<div
+ category="primary"
+ hide-footer=""
+>
+ <div
+ data-testid="slot-modal-title"
+ >
+ myfile.cer Metadata
+ </div>
+ <div
+ data-testid="slot-default"
+ >
+
+ <table
+ aria-busy="false"
+ aria-colcount="2"
+ class="table b-table gl-table"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Name
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Data
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Name
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Apple Distribution: Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Serial
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 33669367788748363528491290218354043267
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Team
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Issuer
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Apple Worldwide Developer Relations Certification Authority - G3
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Expires at
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ April 26, 2022 at 7:20:40 PM GMT
+
+ </td>
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+ </div>
+</div>
+`;
+
+exports[`Secure File Metadata Modal when a .mobileprovision file is supplied matches the mobileprovision snapshot 1`] = `
+<div
+ category="primary"
+ hide-footer=""
+>
+ <div
+ data-testid="slot-modal-title"
+ >
+ sample.mobileprovision Metadata
+ </div>
+ <div
+ data-testid="slot-default"
+ >
+
+ <table
+ aria-busy="false"
+ aria-colcount="2"
+ class="table b-table gl-table"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Name
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Data
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ UUID
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Platforms
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ iOS
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Team
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ App
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ iOS Demo - match Development com.gitlab.ios-demo
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Certificates
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 33669367788748363528491290218354043267
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Expires at
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ August 1, 2023 at 11:15:13 PM GMT
+
+ </td>
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
new file mode 100644
index 00000000000..4ac5b3325d4
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
@@ -0,0 +1,49 @@
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Button from '~/ci_secure_files/components/metadata/button.vue';
+import { secureFiles } from '../../mock_data';
+
+const secureFileWithoutMetadata = secureFiles[0];
+const secureFileWithMetadata = secureFiles[2];
+const modalId = 'metadataModalId';
+
+describe('Secure File Metadata Button', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createWrapper = (secureFile = {}, admin = false) => {
+ wrapper = mount(Button, {
+ propsData: {
+ admin,
+ modalId,
+ secureFile,
+ },
+ });
+ };
+
+ describe('metadata button visibility', () => {
+ it.each`
+ visibility | admin | fileName
+ ${true} | ${true} | ${secureFileWithMetadata}
+ ${false} | ${false} | ${secureFileWithMetadata}
+ ${false} | ${false} | ${secureFileWithoutMetadata}
+ ${false} | ${false} | ${secureFileWithoutMetadata}
+ `(
+ 'button visibility is $visibility when admin equals $admin and $fileName.name is suppled',
+ ({ visibility, admin, fileName }) => {
+ createWrapper(fileName, admin);
+ expect(findButton().exists()).toBe(visibility);
+
+ if (visibility) {
+ expect(findButton().isVisible()).toBe(true);
+ expect(findButton().attributes('aria-label')).toBe('View File Metadata');
+ }
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
new file mode 100644
index 00000000000..230507d32d7
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
@@ -0,0 +1,78 @@
+import { GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+
+import Modal from '~/ci_secure_files/components/metadata/modal.vue';
+
+import { secureFiles } from '../../mock_data';
+
+const cerFile = secureFiles[2];
+const mobileprovisionFile = secureFiles[3];
+const modalId = 'metadataModalId';
+
+describe('Secure File Metadata Modal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const createWrapper = (secureFile = {}) => {
+ wrapper = mount(Modal, {
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
+ },
+ propsData: {
+ modalId,
+ name: secureFile.name,
+ metadata: secureFile.metadata,
+ fileExtension: secureFile.file_extension,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ wrapper.destroy();
+ });
+
+ describe('when a .cer file is supplied', () => {
+ it('matches cer the snapshot', () => {
+ createWrapper(cerFile);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('when a .mobileprovision file is supplied', () => {
+ it('matches the mobileprovision snapshot', () => {
+ createWrapper(mobileprovisionFile);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('event tracking', () => {
+ it('sends cer tracking information when the modal is loaded', () => {
+ createWrapper(cerFile);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
+ expect(trackingSpy).not.toHaveBeenCalledWith(
+ undefined,
+ 'load_secure_file_metadata_mobileprovision',
+ {},
+ );
+ });
+
+ it('sends mobileprovision tracking information when the modal is loaded', () => {
+ createWrapper(mobileprovisionFile);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ 'load_secure_file_metadata_mobileprovision',
+ {},
+ );
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
+ });
+ });
+});
diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
index 5273aafbb04..ab6200ca6f4 100644
--- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
+++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SecureFilesList from '~/ci_secure_files/components/secure_files_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -64,7 +65,7 @@ describe('SecureFilesList', () => {
describe('when secure files exist in a project', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -116,7 +117,7 @@ describe('SecureFilesList', () => {
describe('when no secure files exist in a project', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, []);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, []);
createWrapper();
await waitForPromises();
@@ -137,7 +138,7 @@ describe('SecureFilesList', () => {
describe('pagination', () => {
it('displays the pagination component with there are more than 20 items', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles, { 'x-total': 30 });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles, { 'x-total': 30 });
createWrapper();
await waitForPromises();
@@ -147,7 +148,7 @@ describe('SecureFilesList', () => {
it('does not display the pagination component with there are 20 items', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles, { 'x-total': 20 });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles, { 'x-total': 20 });
createWrapper();
await waitForPromises();
@@ -159,7 +160,7 @@ describe('SecureFilesList', () => {
describe('loading state', () => {
it('displays the loading icon while waiting for the backend request', () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
expect(findLoadingIcon().exists()).toBe(true);
@@ -167,7 +168,7 @@ describe('SecureFilesList', () => {
it('does not display the loading icon after the backend request has completed', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -180,7 +181,7 @@ describe('SecureFilesList', () => {
describe('with admin permissions', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -198,7 +199,7 @@ describe('SecureFilesList', () => {
describe('without admin permissions', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper(false);
await waitForPromises();
diff --git a/spec/frontend/ci_secure_files/mock_data.js b/spec/frontend/ci_secure_files/mock_data.js
index 5a9e16d1ad6..f532b468fb9 100644
--- a/spec/frontend/ci_secure_files/mock_data.js
+++ b/spec/frontend/ci_secure_files/mock_data.js
@@ -4,15 +4,72 @@ export const secureFiles = [
name: 'myfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac',
checksum_algorithm: 'sha256',
- permissions: 'read_only',
created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'jks',
+ metadata: null,
},
{
id: 2,
name: 'myotherfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
checksum_algorithm: 'sha256',
- permissions: 'execute',
created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'jks',
+ metadata: null,
+ },
+ {
+ id: 3,
+ name: 'myfile.cer',
+ checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
+ checksum_algorithm: 'sha256',
+ created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'cer',
+ expires_at: '2022-04-26T19:20:40.000Z',
+ metadata: {
+ id: '33669367788748363528491290218354043267',
+ issuer: {
+ C: 'US',
+ O: 'Apple Inc.',
+ CN: 'Apple Worldwide Developer Relations Certification Authority',
+ OU: 'G3',
+ },
+ subject: {
+ C: 'US',
+ O: 'Team Name',
+ CN: 'Apple Distribution: Team Name (ABC123XYZ)',
+ OU: 'ABC123XYZ',
+ UID: 'ABC123XYZ',
+ },
+ expires_at: '2022-04-26T19:20:40.000Z',
+ },
+ },
+ {
+ id: 4,
+ name: 'sample.mobileprovision',
+ checksum: '9e194bbde00d57c64b6640ed2c9e166d76b4c79d9dbd49770f95be56678f2a62',
+ checksum_algorithm: 'sha256',
+ created_at: '2022-11-15T19:29:57.577Z',
+ expires_at: '2023-08-01T23:15:13.000Z',
+ metadata: {
+ id: '6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf',
+ app_id: 'match Development com.gitlab.ios-demo',
+ devices: ['00008101-001454860C10001E'],
+ team_id: ['ABC123XYZ'],
+ app_name: 'iOS Demo',
+ platforms: ['iOS'],
+ team_name: 'Team Name',
+ expires_at: '2023-08-01T18:15:13.000-05:00',
+ entitlements: {
+ 'get-task-allow': true,
+ 'application-identifier': 'N7SYAN8PX8.com.gitlab.ios-demo',
+ 'keychain-access-groups': ['ABC123XYZ.*', 'com.apple.token'],
+ 'com.apple.developer.game-center': true,
+ 'com.apple.developer.team-identifier': 'ABC123XYZ',
+ },
+ app_id_prefix: ['ABC123XYZ'],
+ xcode_managed: false,
+ certificate_ids: ['33669367788748363528491290218354043267'],
+ },
+ file_extension: 'mobileprovision',
},
];
diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
index 01eb08f4ece..f1df4208fa2 100644
--- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
+++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
@@ -1,6 +1,6 @@
import { GlTable, GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -11,7 +11,7 @@ describe('TriggersList', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = mount(TriggersList, {
+ wrapper = mountExtended(TriggersList, {
propsData: { triggers, ...props },
});
};
@@ -25,59 +25,75 @@ describe('TriggersList', () => {
const findInvalidBadge = (i) => findCell(i, 0).findComponent(GlBadge);
const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
+ const findRevealHideButton = () => wrapper.findByTestId('reveal-hide-values-button');
- beforeEach(async () => {
- createComponent();
+ describe('With triggers set', () => {
+ beforeEach(async () => {
+ createComponent();
- await nextTick();
- });
+ await nextTick();
+ });
- it('displays a table with expected headers', () => {
- const headers = ['Token', 'Description', 'Owner', 'Last Used', ''];
- headers.forEach((header, i) => {
- expect(findHeaderAt(i).text()).toBe(header);
+ it('displays a table with expected headers', () => {
+ const headers = ['Token', 'Description', 'Owner', 'Last Used', ''];
+ headers.forEach((header, i) => {
+ expect(findHeaderAt(i).text()).toBe(header);
+ });
});
- });
- it('displays a table with rows', () => {
- expect(findRows()).toHaveLength(triggers.length);
+ it('displays a "Reveal/Hide values" button', async () => {
+ const revealHideButton = findRevealHideButton();
- const [trigger] = triggers;
+ expect(revealHideButton.exists()).toBe(true);
+ expect(revealHideButton.text()).toBe('Reveal values');
- expect(findCell(0, 0).text()).toBe(trigger.token);
- expect(findCell(0, 1).text()).toBe(trigger.description);
- expect(findCell(0, 2).text()).toContain(trigger.owner.name);
- });
+ await revealHideButton.vm.$emit('click');
- it('displays a "copy to cliboard" button for exposed tokens', () => {
- expect(findClipboardBtn(0).exists()).toBe(true);
- expect(findClipboardBtn(0).props('text')).toBe(triggers[0].token);
+ expect(revealHideButton.text()).toBe('Hide values');
+ });
- expect(findClipboardBtn(1).exists()).toBe(false);
- });
+ it('displays a table with rows', async () => {
+ await findRevealHideButton().vm.$emit('click');
- it('displays an "invalid" label for tokens without access', () => {
- expect(findInvalidBadge(0).exists()).toBe(false);
+ expect(findRows()).toHaveLength(triggers.length);
- expect(findInvalidBadge(1).exists()).toBe(true);
- });
+ const [trigger] = triggers;
- it('displays a time ago label when last used', () => {
- expect(findCell(0, 3).text()).toBe('Never');
+ expect(findCell(0, 0).text()).toBe(trigger.token);
+ expect(findCell(0, 1).text()).toBe(trigger.description);
+ expect(findCell(0, 2).text()).toContain(trigger.owner.name);
+ });
- expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
- });
+ it('displays a "copy to cliboard" button for exposed tokens', () => {
+ expect(findClipboardBtn(0).exists()).toBe(true);
+ expect(findClipboardBtn(0).props('text')).toBe(triggers[0].token);
- it('displays actions in a rows', () => {
- const [data] = triggers;
- const confirmWarning =
- 'By revoking a trigger you will break any processes making use of it. Are you sure?';
+ expect(findClipboardBtn(1).exists()).toBe(false);
+ });
+
+ it('displays an "invalid" label for tokens without access', () => {
+ expect(findInvalidBadge(0).exists()).toBe(false);
- expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
+ expect(findInvalidBadge(1).exists()).toBe(true);
+ });
- expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
- expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
- expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning);
+ it('displays a time ago label when last used', () => {
+ expect(findCell(0, 3).text()).toBe('Never');
+
+ expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
+ });
+
+ it('displays actions in a rows', () => {
+ const [data] = triggers;
+ const confirmWarning =
+ 'By revoking a trigger you will break any processes making use of it. Are you sure?';
+
+ expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
+
+ expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
+ expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
+ expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning);
+ });
});
describe('when there are no triggers set', () => {
diff --git a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
index 2af64191a88..db1219ccb41 100644
--- a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
+++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
@@ -31,7 +31,7 @@ describe('IntegrationStatus', () => {
describe('icon', () => {
const icon = 'status-success';
- const iconClass = 'text-success-500';
+ const iconClass = 'gl-text-green-500';
it.each`
props | iconName | iconClassName
${{ icon, iconClass }} | ${icon} | ${iconClass}
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index ad2aa4acbaf..a2ec19c5b4a 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -3,10 +3,11 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import Clusters from '~/clusters/clusters_bundle';
import axios from '~/lib/utils/axios_utils';
-import initProjectSelectDropdown from '~/project_select';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects';
jest.mock('~/lib/utils/poll');
-jest.mock('~/project_select');
+jest.mock('~/vue_shared/components/entity_select/init_project_selects');
useMockLocationHelper();
@@ -19,7 +20,7 @@ describe('Clusters', () => {
mock = new MockAdapter(axios);
- mock.onGet(statusPath).reply(200);
+ mock.onGet(statusPath).reply(HTTP_STATUS_OK);
};
beforeEach(() => {
@@ -48,7 +49,7 @@ describe('Clusters', () => {
});
it('should call initProjectSelectDropdown on construct', () => {
- expect(initProjectSelectDropdown).toHaveBeenCalled();
+ expect(initProjectSelects).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index e8e705a6384..20dbff9df15 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -7,6 +7,7 @@ import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { apiData } from '../mock_data';
describe('Clusters', () => {
@@ -68,7 +69,7 @@ describe('Clusters', () => {
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios);
- mockPollingApi(200, apiData, paginationHeader());
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader());
return createWrapper({});
});
@@ -255,7 +256,7 @@ describe('Clusters', () => {
const totalSecondPage = 500;
beforeEach(() => {
- mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1));
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalFirstPage, perPage, 1));
return createWrapper({});
});
@@ -269,7 +270,7 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
- mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2));
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2));
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentPage: 2 });
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 1deebf8b75a..360fd3b2842 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -7,6 +7,7 @@ import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
@@ -65,7 +66,7 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', () => {
- mock.onGet().reply(200, apiData, headers);
+ mock.onGet().reply(HTTP_STATUS_OK, apiData, headers);
return testAction(
actions.fetchClusters,
@@ -81,7 +82,7 @@ describe('Clusters store actions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet().reply(400, 'Not Found');
+ mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
actions.fetchClusters,
@@ -118,7 +119,7 @@ describe('Clusters store actions', () => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
pollStop = jest.spyOn(Poll.prototype, 'stop');
- mock.onGet().reply(200, apiData, pollHeaders);
+ mock.onGet().reply(HTTP_STATUS_OK, apiData, pollHeaders);
});
afterEach(() => {
@@ -171,7 +172,7 @@ describe('Clusters store actions', () => {
it('should stop polling and report to Sentry when data is invalid', async () => {
const badApiResponse = { clusters: {} };
- mock.onGet().reply(200, badApiResponse, pollHeaders);
+ mock.onGet().reply(HTTP_STATUS_OK, badApiResponse, pollHeaders);
await testAction(
actions.fetchClusters,
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 8eee61d1342..ab5d7fce905 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/code_navigation/utils');
@@ -45,7 +46,7 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(codeNavigationPath).replyOnce(200, [
+ mock.onGet(codeNavigationPath).replyOnce(HTTP_STATUS_OK, [
{
start_line: 0,
start_char: 0,
@@ -124,7 +125,7 @@ describe('Code navigation actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(codeNavigationPath).replyOnce(500);
+ mock.onGet(codeNavigationPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestDataError', () => {
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 16737003fa0..debd10de118 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -141,6 +141,16 @@ describe('Commit box pipeline mini graph', () => {
expect(upstreamPipeline).toEqual(null);
});
+ it('should render the latest downstream pipeline only', async () => {
+ createComponent(downstreamHandler);
+
+ await waitForPromises();
+
+ const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
+
+ expect(downstreamPipelines).toHaveLength(1);
+ });
+
it('should pass the pipeline path prop for the counter badge', async () => {
createComponent(downstreamHandler);
@@ -173,7 +183,14 @@ describe('Commit box pipeline mini graph', () => {
const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
expect(upstreamPipeline).toEqual(samplePipeline);
- expect(downstreamPipelines).toEqual(expect.arrayContaining([samplePipeline]));
+ expect(downstreamPipelines).toEqual(
+ expect.arrayContaining([
+ {
+ ...samplePipeline,
+ sourceJob: expect.any(Object),
+ },
+ ]),
+ );
});
});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index aef137e6fa5..a13ef9c563e 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -1,3 +1,90 @@
+export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-611-611',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+});
+
+const upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-610-610',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+};
+
export const mockStages = [
{
name: 'build',
@@ -74,24 +161,7 @@ export const mockDownstreamQueryResponse = {
pipeline: {
path: '/root/ci-project/-/pipelines/790',
id: 'pipeline-1',
- downstream: {
- nodes: [
- {
- id: 'gid://gitlab/Ci::Pipeline/612',
- path: '/root/job-log-sections/-/pipelines/612',
- project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
- ],
- __typename: 'PipelineConnection',
- },
+ downstream: mockDownstreamPipelinesGraphql(),
upstream: null,
},
__typename: 'Project',
@@ -106,37 +176,8 @@ export const mockUpstreamDownstreamQueryResponse = {
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
- downstream: {
- nodes: [
- {
- id: 'gid://gitlab/Ci::Pipeline/612',
- path: '/root/job-log-sections/-/pipelines/612',
- project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
- ],
- __typename: 'PipelineConnection',
- },
- upstream: {
- id: 'gid://gitlab/Ci::Pipeline/610',
- path: '/root/trigger-downstream/-/pipelines/610',
- project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
+ downstream: mockDownstreamPipelinesGraphql(),
+ upstream,
},
__typename: 'Project',
},
@@ -154,19 +195,7 @@ export const mockUpstreamQueryResponse = {
nodes: [],
__typename: 'PipelineConnection',
},
- upstream: {
- id: 'gid://gitlab/Ci::Pipeline/610',
- path: '/root/trigger-downstream/-/pipelines/610',
- project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
+ upstream,
},
__typename: 'Project',
},
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 6865b721441..4bffb6a0fd3 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -10,6 +10,7 @@ import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
@@ -69,7 +70,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('successful request', () => {
describe('without pipelines', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent();
@@ -96,7 +97,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('with pipelines', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, [pipeline], { 'x-total': 10 });
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 });
createComponent();
@@ -168,7 +169,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
pipelineCopy.flags.merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent();
@@ -184,7 +185,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipelineCopy.flags.detached_merge_request_pipeline = false;
pipelineCopy.flags.merge_request_pipeline = false;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent();
@@ -199,7 +200,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(async () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
canRunPipeline: true,
@@ -277,7 +278,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(async () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
projectId: '5',
@@ -309,7 +310,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('when no pipelines were created on a forked merge request', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent({
projectId: '5',
@@ -337,7 +338,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('unsuccessfull request', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(500, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []);
createComponent();
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
index db1516ed4ec..c79170aa37e 100644
--- a/spec/frontend/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -4,6 +4,7 @@ import 'vendor/jquery.endless-scroll';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import CommitsList from '~/commits';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Pager from '~/pager';
describe('Commits List', () => {
@@ -64,7 +65,7 @@ describe('Commits List', () => {
jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
mock = new MockAdapter(axios);
- mock.onGet('/h5bp/html5-boilerplate/commits/main').reply(200, {
+ mock.onGet('/h5bp/html5-boilerplate/commits/main').reply(HTTP_STATUS_OK, {
html: '<li>Result</li>',
});
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
index 770f2636648..4d577fe1132 100644
--- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -1,47 +1,79 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
-let vm;
+const TEST_PROJECTS = [
+ {
+ id: 7,
+ name: 'test',
+ },
+ {
+ id: 9,
+ name: 'lorem ipsum',
+ },
+ {
+ id: 11,
+ name: 'dolar sit',
+ },
+];
-function factory(projects = []) {
- vm = mount(Dropdown, {
- propsData: {
- projects,
- selectedProject: projects[0],
- },
- });
-}
-
-describe('Confidential merge request project dropdown component', () => {
- afterEach(() => {
- vm.destroy();
- });
+describe('~/confidential_merge_request/components/dropdown.vue', () => {
+ let wrapper;
- it('renders dropdown items', () => {
- factory([
- {
- id: 1,
- name: 'test',
- },
- {
- id: 2,
- name: 'test',
+ function factory(props = {}) {
+ wrapper = shallowMount(Dropdown, {
+ propsData: {
+ projects: TEST_PROJECTS,
+ ...props,
},
- ]);
+ });
+ }
- expect(vm.findAllComponents(GlDropdownItem).length).toBe(2);
- });
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ describe('default', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('renders collapsible listbox', () => {
+ expect(findListbox().props()).toMatchObject({
+ icon: 'lock',
+ selected: [],
+ toggleText: 'Select private project',
+ block: true,
+ items: TEST_PROJECTS.map(({ id, name }) => ({
+ value: String(id),
+ text: name,
+ })),
+ });
+ });
+
+ it('does not emit anything', () => {
+ expect(wrapper.emitted()).toEqual({});
+ });
- it('shows lock icon', () => {
- factory();
+ describe('when listbox emits selected', () => {
+ beforeEach(() => {
+ findListbox().vm.$emit('select', String(TEST_PROJECTS[1].id));
+ });
- expect(vm.findComponent(GlDropdown).props('icon')).toBe('lock');
+ it('emits selected project', () => {
+ expect(wrapper.emitted('select')).toEqual([[TEST_PROJECTS[1]]]);
+ });
+ });
});
- it('has dropdown text', () => {
- factory();
+ describe('with selected', () => {
+ beforeEach(() => {
+ factory({ selectedProject: TEST_PROJECTS[1] });
+ });
- expect(vm.findComponent(GlDropdown).props('text')).toBe('Select private project');
+ it('shows selected project', () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: String(TEST_PROJECTS[1].id),
+ toggleText: TEST_PROJECTS[1].name,
+ });
+ });
});
});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index 0e73d50fdb5..d6f16f1a644 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const mockData = [
{
@@ -27,7 +28,7 @@ let mock;
function factory(projects = mockData) {
mock = new MockAdapter(axios);
- mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
+ mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(HTTP_STATUS_OK, projects);
wrapper = shallowMount(ProjectFormGroup, {
propsData: {
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index a23f8370adf..d4fc47601cf 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -1,10 +1,9 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue';
import Diagram from '~/content_editor/extensions/diagram';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
import eventHubFactory from '~/helpers/event_hub_factory';
-import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_more_dropdown', () => {
@@ -25,14 +24,11 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
tiptapEditor,
eventHub,
},
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
propsData,
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
beforeEach(() => {
buildEditor();
@@ -60,7 +56,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
beforeEach(async () => {
commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']);
- btn = wrapper.findByRole('menuitem', { name });
+ btn = wrapper.findByRole('button', { name });
});
it(`inserts a ${contentType}`, async () => {
@@ -76,12 +72,11 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ it('sets toggleText and text-sr-only properties to the table button dropdown', () => {
expect(findDropdown().props()).toMatchObject({
- text: 'More',
textSrOnly: true,
+ toggleText: 'More options',
});
- expect(findDropdown().attributes('title')).toBe('More');
});
});
});
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 3f812d3cf4e..2f441f0f747 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -1,9 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = `
+exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
<div>
<div
- class="contributors-charts"
+ class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <div
+ class="gl-mr-3"
+ >
+ <refselector-stub
+ enabledreftypes="REF_TYPE_BRANCHES,REF_TYPE_TAGS"
+ name=""
+ projectid="23"
+ state="true"
+ translations="[object Object]"
+ value="main"
+ />
+ </div>
+
+ <a
+ class="btn btn-default btn-md gl-button"
+ data-testid="history-button"
+ href="some/path"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ History
+
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <div
+ data-testid="contributors-charts"
>
<h4
class="gl-mb-2 gl-mt-5"
@@ -49,8 +87,8 @@ exports[`Contributors charts should render charts when loading completed and the
class="gl-mb-3"
>
- 2 commits (jawnnypoo@gmail.com)
-
+ 2 commits (jawnnypoo@gmail.com)
+
</p>
<div>
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 2f0b5719326..03b1e977548 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,41 +1,58 @@
-import { mount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { visitUrl } from '~/lib/utils/url_utility';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
let wrapper;
let mock;
let store;
const Component = Vue.extend(ContributorsCharts);
-const endpoint = 'contributors';
+const endpoint = 'contributors/-/graphs';
const branch = 'main';
const chartData = [
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
];
+const projectId = '23';
+const commitsPath = 'some/path';
function factory() {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
- mock.onGet().reply(200, chartData);
+ mock.onGet().reply(HTTP_STATUS_OK, chartData);
store = createStore();
- wrapper = mount(Component, {
+ wrapper = mountExtended(Component, {
propsData: {
endpoint,
branch,
+ projectId,
+ commitsPath,
},
stubs: {
GlLoadingIcon: true,
GlAreaChart: true,
+ RefSelector: true,
},
store,
});
}
+const findLoadingIcon = () => wrapper.findByTestId('loading-app-icon');
+const findRefSelector = () => wrapper.findComponent(RefSelector);
+const findHistoryButton = () => wrapper.findByTestId('history-button');
+const findContributorsCharts = () => wrapper.findByTestId('contributors-charts');
+
describe('Contributors charts', () => {
beforeEach(() => {
factory();
@@ -53,15 +70,46 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', async () => {
wrapper.vm.$store.state.loading = true;
await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('should render charts when loading completed and there is chart data', async () => {
+ it('should render charts and a RefSelector when loading completed and there is chart data', async () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find('.contributors-charts').exists()).toBe(true);
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findRefSelector().exists()).toBe(true);
+ expect(findRefSelector().props()).toMatchObject({
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: branch,
+ projectId,
+ translations: { dropdownHeader: 'Switch branch/tag' },
+ useSymbolicRefNames: false,
+ state: true,
+ name: '',
+ });
+ expect(findContributorsCharts().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
+
+ it('should have a history button with a set href attribute', async () => {
+ wrapper.vm.$store.state.loading = false;
+ wrapper.vm.$store.state.chartData = chartData;
+ await nextTick();
+
+ const historyButton = findHistoryButton();
+ expect(historyButton.exists()).toBe(true);
+ expect(historyButton.attributes('href')).toBe(commitsPath);
+ });
+
+ it('visits a URL when clicking on a branch/tag', async () => {
+ wrapper.vm.$store.state.loading = false;
+ wrapper.vm.$store.state.chartData = chartData;
+ await nextTick();
+
+ findRefSelector().vm.$emit('input', branch);
+
+ expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`);
+ });
});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index 865f683a91a..b2ebdf2f53c 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash.js');
@@ -22,7 +23,7 @@ describe('Contributors store actions', () => {
});
it('should commit SET_CHART_DATA with received response', () => {
- mock.onGet().reply(200, chartData);
+ mock.onGet().reply(HTTP_STATUS_OK, chartData);
return testAction(
actions.fetchChartData,
@@ -38,7 +39,7 @@ describe('Contributors store actions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet().reply(400, 'Not Found');
+ mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
actions.fetchChartData,
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index bd4ed950f9d..7d9ae548c9a 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('custom metrics form fields component', () => {
let wrapper;
@@ -46,7 +47,7 @@ describe('custom metrics form fields component', () => {
});
it('checks form validity', async () => {
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
mountComponent({
metricPersisted: true,
...makeFormData({
@@ -143,7 +144,7 @@ describe('custom metrics form fields component', () => {
describe('when query validation is in flight', () => {
beforeEach(() => {
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) });
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
});
it('expect loading message to display', async () => {
@@ -168,7 +169,7 @@ describe('custom metrics form fields component', () => {
const invalidQueryResponse = { success: true, query: { valid: false, error: errorMessage } };
beforeEach(() => {
- mockAxios.onPost(validateQueryPath).reply(200, invalidQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, invalidQueryResponse);
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'invalidQuery' }) });
return axios.waitForAll();
});
@@ -180,7 +181,7 @@ describe('custom metrics form fields component', () => {
describe('when query is valid', () => {
beforeEach(() => {
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) });
});
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 79a9aaa9184..d11ecf95de6 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -8,6 +8,7 @@ import deployKeysApp from '~/deploy_keys/components/app.vue';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
import eventHub from '~/deploy_keys/eventhub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
@@ -28,7 +29,7 @@ describe('Deploy keys app component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(TEST_ENDPOINT).reply(200, data);
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, data);
});
afterEach(() => {
@@ -67,7 +68,7 @@ describe('Deploy keys app component', () => {
});
it('does not render key panels when keys object is empty', () => {
- mock.onGet(TEST_ENDPOINT).reply(200, []);
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, []);
return mountComponent().then(() => {
expect(findKeyPanels().length).toBe(0);
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 0bf69acd251..46f7b2f3604 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import { GlButton, GlFormCheckbox, GlFormInput, GlFormInputGroup, GlDatepicker } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -131,7 +132,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(500, { message: expectedErrorMessage });
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: expectedErrorMessage });
wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
@@ -183,7 +184,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(200, { username: 'test token username', token: 'test token' });
+ .replyOnce(HTTP_STATUS_OK, { username: 'test token username', token: 'test token' });
return submitTokenThenCheck();
});
@@ -216,7 +217,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(200, { username: 'test token username', token: 'test token' });
+ .replyOnce(HTTP_STATUS_OK, { username: 'test token username', token: 'test token' });
return submitTokenThenCheck();
});
diff --git a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
index 7cffd3cf3e8..1f4e579f075 100644
--- a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
@@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Design management large image component renders SVG with proper height and width 1`] = `
+<div
+ class="gl-mx-auto gl-my-auto js-design-image"
+>
+ <!---->
+
+ <img
+ alt="test"
+ class="mh-100 img-fluid"
+ src="mockImage.svg"
+ />
+</div>
+`;
+
exports[`Design management large image component renders image 1`] = `
<div
class="gl-mx-auto gl-my-auto js-design-image"
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 169f2dbdccb..2807fe7727f 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,32 +1,54 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+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 DesignOverlay from '~/design_management/components/design_overlay.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
-import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import { resolvers } from '~/design_management/graphql';
+import activeDiscussionQuery from '~/design_management/graphql/queries/active_discussion.query.graphql';
import notes from '../mock_data/notes';
-const mutate = jest.fn(() => Promise.resolve());
+Vue.use(VueApollo);
describe('Design overlay component', () => {
let wrapper;
+ let apolloProvider;
const mockDimensions = { width: 100, height: 100 };
- const findAllNotes = () => wrapper.findAll('.js-image-badge');
- const findCommentBadge = () => wrapper.find('.comment-indicator');
+ const findOverlay = () => wrapper.find('[data-testid="design-overlay"]');
+ const findAllNotes = () => wrapper.findAll('[data-testid="note-pin"]');
+ const findCommentBadge = () => wrapper.find('[data-testid="comment-badge"]');
const findBadgeAtIndex = (noteIndex) => findAllNotes().at(noteIndex);
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
- elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
+ elem.vm.$emit(
+ 'mousedown',
+ new MouseEvent('click', { clientX: fromPoint.x, clientY: fromPoint.y }),
+ );
+ findOverlay().trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
await nextTick();
- elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
+ elem.vm.$emit('mouseup', new MouseEvent('click', { clientX: toPoint.x, clientY: toPoint.y }));
await nextTick();
};
function createComponent(props = {}, data = {}) {
- wrapper = mount(DesignOverlay, {
+ apolloProvider = createMockApollo([], resolvers);
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: null,
+ source: null,
+ },
+ },
+ });
+
+ wrapper = shallowMount(DesignOverlay, {
+ apolloProvider,
propsData: {
dimensions: mockDimensions,
position: {
@@ -45,14 +67,13 @@ describe('Design overlay component', () => {
...data,
};
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
});
}
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
it('should have correct inline style', () => {
createComponent();
@@ -96,12 +117,15 @@ describe('Design overlay component', () => {
});
it('should have set the correct position for each note badge', () => {
- expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;');
- expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;');
+ expect(findFirstBadge().props('position')).toEqual({
+ left: '10px',
+ top: '15px',
+ });
+ expect(findSecondBadge().props('position')).toEqual({ left: '50px', top: '50px' });
});
it('should apply resolved class to the resolved note pin', () => {
- expect(findSecondBadge().classes()).toContain('resolved');
+ expect(findSecondBadge().props('isResolved')).toBe(true);
});
describe('when no discussion is active', () => {
@@ -116,33 +140,37 @@ describe('Design overlay component', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
async (note) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- activeDiscussion: {
- id: note.id,
- source: 'discussion',
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: note.id,
+ source: 'discussion',
+ },
},
});
await nextTick();
- expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
+ expect(findBadgeAtIndex(0).props('isInactive')).toBe(false);
},
);
it('should apply inactive class to all pins besides the active one', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- activeDiscussion: {
- id: notes[0].id,
- source: 'discussion',
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: notes[0].id,
+ source: 'discussion',
+ },
},
});
await nextTick();
- expect(findSecondBadge().classes()).toContain('inactive');
- expect(findFirstBadge().classes()).not.toContain('inactive');
+ expect(findSecondBadge().props('isInactive')).toBe(true);
+ expect(findFirstBadge().props('isInactive')).toBe(false);
});
});
});
@@ -156,7 +184,7 @@ describe('Design overlay component', () => {
},
});
- expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
+ expect(findFirstBadge().props('position')).toEqual({ left: '40px', top: '60px' });
wrapper.setProps({
dimensions: {
@@ -166,10 +194,10 @@ describe('Design overlay component', () => {
});
await nextTick();
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
+ expect(findFirstBadge().props('position')).toEqual({ left: '20px', top: '30px' });
});
- it('should call an update active discussion mutation when clicking a note without moving it', async () => {
+ it('should update active discussion when clicking a note without moving it', async () => {
createComponent({
notes,
dimensions: {
@@ -178,61 +206,36 @@ describe('Design overlay component', () => {
},
});
+ expect(findFirstBadge().props('isInactive')).toBe(null);
+
const note = notes[0];
const { position } = note;
- const mutationVariables = {
- mutation: updateActiveDiscussion,
- variables: {
- id: note.id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
- },
- };
- findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y });
+ findFirstBadge().vm.$emit(
+ 'mousedown',
+ new MouseEvent('click', { clientX: position.x, clientY: position.y }),
+ );
await nextTick();
- findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ findFirstBadge().vm.$emit(
+ 'mouseup',
+ new MouseEvent('click', { clientX: position.x, clientY: position.y }),
+ );
+ await waitForPromises();
+ expect(findFirstBadge().props('isInactive')).toBe(false);
});
});
describe('when moving notes', () => {
- it('should update badge style when note is being moved', async () => {
- createComponent({
- notes,
- });
-
- const { position } = notes[0];
-
- await clickAndDragBadge(findFirstBadge(), { x: position.x, y: position.y }, { x: 20, y: 20 });
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
-
it('should emit `moveNote` event when note-moving action ends', async () => {
createComponent({ notes });
const note = notes[0];
const { position } = note;
const newCoordinates = { x: 20, y: 20 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteNewPosition: {
- ...position,
- ...newCoordinates,
- },
- movingNoteStartPosition: {
- noteId: notes[0].id,
- discussionId: notes[0].discussion.id,
- ...position,
- },
- });
-
const badge = findFirstBadge();
await clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates);
- badge.trigger('mouseup');
- await nextTick();
expect(wrapper.emitted('moveNote')).toEqual([
[
{
@@ -266,9 +269,10 @@ describe('Design overlay component', () => {
const badge = findAllNotes().at(0);
await clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 });
// note position should not change after a click-and-drag attempt
- expect(findFirstBadge().attributes().style).toContain(
- `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`,
- );
+ expect(findFirstBadge().props('position')).toEqual({
+ left: `${mockNoteCoordinates.x}px`,
+ top: `${mockNoteCoordinates.y}px`,
+ });
});
});
});
@@ -282,27 +286,10 @@ describe('Design overlay component', () => {
});
expect(findCommentBadge().exists()).toBe(true);
- expect(findCommentBadge().attributes().style).toBe('left: 10px; top: 15px;');
+ expect(findCommentBadge().props('position')).toEqual({ left: '10px', top: '15px' });
});
describe('when moving the comment badge', () => {
- it('should update badge style to reflect new position', async () => {
- const { position } = notes[0];
-
- createComponent({
- currentCommentForm: {
- ...position,
- },
- });
-
- await clickAndDragBadge(
- findCommentBadge(),
- { x: position.x, y: position.y },
- { x: 20, y: 20 },
- );
- expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
-
it('should update badge style when note-moving action ends', async () => {
const { position } = notes[0];
createComponent({
@@ -315,7 +302,7 @@ describe('Design overlay component', () => {
const toPoint = { x: 20, y: 20 };
await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
- commentBadge.trigger('mouseup');
+ commentBadge.vm.$emit('mouseup', new MouseEvent('click'));
// simulates the currentCommentForm being updated in index.vue component, and
// propagated back down to this prop
wrapper.setProps({
@@ -323,110 +310,50 @@ describe('Design overlay component', () => {
});
await nextTick();
- expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
+ expect(commentBadge.props('position')).toEqual({ left: '20px', top: '20px' });
});
- it.each`
- element | getElementFunc | event
- ${'overlay'} | ${() => wrapper} | ${'mouseleave'}
- ${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
- `(
- 'should emit `openCommentForm` event when $event fired on $element element',
- async ({ getElementFunc, event }) => {
- createComponent({
- notes,
- currentCommentForm: {
- ...notes[0].position,
- },
- });
-
- const newCoordinates = { x: 20, y: 20 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteStartPosition: {
- ...notes[0].position,
- },
- movingNoteNewPosition: {
- ...notes[0].position,
- ...newCoordinates,
- },
- });
-
- getElementFunc().trigger(event);
- await nextTick();
- expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
- },
- );
- });
- });
-
- describe('getMovingNotePositionDelta', () => {
- it('should calculate delta correctly from state', () => {
- createComponent();
+ it('should emit `openCommentForm` event when mouseleave fired on overlay element', async () => {
+ const { position } = notes[0];
+ createComponent({
+ notes,
+ currentCommentForm: {
+ ...position,
+ },
+ });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteStartPosition: {
- clientX: 10,
- clientY: 20,
- },
- });
+ const newCoordinates = { x: 20, y: 20 };
- const mockMouseEvent = {
- clientX: 30,
- clientY: 10,
- };
+ await clickAndDragBadge(
+ findCommentBadge(),
+ { x: position.x, y: position.y },
+ newCoordinates,
+ );
- expect(wrapper.vm.getMovingNotePositionDelta(mockMouseEvent)).toEqual({
- deltaX: 20,
- deltaY: -10,
+ wrapper.trigger('mouseleave');
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
});
- });
- });
- describe('isPositionInOverlay', () => {
- createComponent({ dimensions: mockDimensions });
-
- it.each`
- test | coordinates | expectedResult
- ${'within overlay bounds'} | ${{ x: 50, y: 50 }} | ${true}
- ${'outside overlay bounds'} | ${{ x: 101, y: 101 }} | ${false}
- `('returns [$expectedResult] when position is $test', ({ coordinates, expectedResult }) => {
- const position = { ...mockDimensions, ...coordinates };
+ it('should emit `openCommentForm` event when mouseup fired on comment badge element', async () => {
+ const { position } = notes[0];
+ createComponent({
+ notes,
+ currentCommentForm: {
+ ...position,
+ },
+ });
- expect(wrapper.vm.isPositionInOverlay(position)).toBe(expectedResult);
- });
- });
+ const newCoordinates = { x: 20, y: 20 };
- describe('getNoteRelativePosition', () => {
- it('calculates position correctly', () => {
- createComponent({ dimensions: mockDimensions });
- const position = { x: 50, y: 50, width: 200, height: 200 };
+ await clickAndDragBadge(
+ findCommentBadge(),
+ { x: position.x, y: position.y },
+ newCoordinates,
+ );
- expect(wrapper.vm.getNoteRelativePosition(position)).toEqual({ left: 25, top: 25 });
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
+ });
});
});
-
- describe('canMoveNote', () => {
- it.each`
- repositionNotePermission | canMoveNoteResult
- ${true} | ${true}
- ${false} | ${false}
- ${undefined} | ${false}
- `(
- 'returns [$canMoveNoteResult] when [repositionNote permission] is [$repositionNotePermission]',
- ({ repositionNotePermission, canMoveNoteResult }) => {
- createComponent();
-
- const note = {
- userPermissions: {
- repositionNote: repositionNotePermission,
- },
- };
- expect(wrapper.vm.canMoveNote(note)).toBe(canMoveNoteResult);
- },
- );
- });
});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 8163cb0d87a..95d2ad504de 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -42,6 +42,16 @@ describe('Design management large image component', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('renders SVG with proper height and width', () => {
+ createComponent({
+ isLoading: false,
+ image: 'mockImage.svg',
+ name: 'test',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
it('sets correct classes and styles if imageStyle is set', async () => {
createComponent(
{
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 096d776a7d2..3517c0f7a44 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -21,12 +21,14 @@ exports[`Design management list item component with notes renders item with mult
>
<!---->
- <gl-intersection-observer-stub>
+ <gl-intersection-observer-stub
+ class="gl-flex-grow-1"
+ >
<!---->
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
@@ -98,12 +100,14 @@ exports[`Design management list item component with notes renders item with sing
>
<!---->
- <gl-intersection-observer-stub>
+ <gl-intersection-observer-stub
+ class="gl-flex-grow-1"
+ >
<!---->
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 66d3f883960..e907e2e4ac5 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -160,9 +160,9 @@ describe('Design management list item component', () => {
describe('with associated event', () => {
it.each`
event | icon | className
- ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'}
- ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'}
- ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'}
+ ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'gl-text-blue-500'}
+ ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'gl-text-red-500'}
+ ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'gl-text-green-500'}
`('renders item with correct status icon for $event event', ({ event, icon, className }) => {
createComponent({ event });
const eventIcon = findEventIcon();
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
deleted file mode 100644
index a4af73dd194..00000000000
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ /dev/null
@@ -1,243 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- issueiid=""
- projectpath=""
- size="small"
- toggleid="dropdown-toggle-btn-2"
- toggletext="Showing latest version"
- variant="default"
->
-
- <!---->
-
- <!---->
-
- <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"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
- ischeckcentered="true"
- >
- <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
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
- ischeckcentered="true"
- >
- <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-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- issueiid=""
- projectpath=""
- size="small"
- toggleid="dropdown-toggle-btn-4"
- toggletext="Showing latest version"
- variant="default"
->
-
- <!---->
-
- <!---->
-
- <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"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
- ischeckcentered="true"
- >
- <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
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
- ischeckcentered="true"
- >
- <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 1e9f286a0ec..6ad10e707ab 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
@@ -32,7 +32,7 @@ describe('Design management design version dropdown component', () => {
mocks: {
$route,
},
- stubs: { GlAvatar, GlCollapsibleListbox },
+ stubs: { GlAvatar: true, GlCollapsibleListbox },
});
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
@@ -50,18 +50,28 @@ describe('Design management design version dropdown component', () => {
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
- it('renders design version dropdown button', async () => {
- createComponent();
+ describe('renders the item with custom template in design version list', () => {
+ let listItem;
+ const latestVersion = mockAllVersions[0];
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
- });
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+ listItem = findAllListboxItems().at(0);
+ });
- it('renders design version list', async () => {
- createComponent();
+ it('should render author name and their avatar', () => {
+ expect(listItem.findComponent(GlAvatar).props('alt')).toBe(latestVersion.author.name);
+ expect(listItem.text()).toContain(latestVersion.author.name);
+ });
+
+ it('should render correct version number', () => {
+ expect(listItem.text()).toContain('Version 2 (latest)');
+ });
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
+ it('should render time ago tooltip', () => {
+ expect(listItem.findComponent(TimeAgo).props('time')).toBe(latestVersion.createdAt);
+ });
});
describe('selected version name', () => {
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index c8be0bedb4c..513e67ea247 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -17,6 +17,7 @@ import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vu
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
@@ -87,7 +88,7 @@ describe('diffs/components/app', () => {
};
window.mrTabs.expandViewContainer = jest.fn();
mock = new MockAdapter(axios);
- mock.onGet(TEST_ENDPOINT).reply(200, {});
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, {});
});
afterEach(() => {
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 75d55376d09..08be3fa2745 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -9,8 +9,8 @@ import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_sta
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
-const TEST_SIGNATURE_HTML = `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
- Verified
+const TEST_SIGNATURE_HTML = `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
+ <span class="gl-badge badge badge-pill badge-success md">Verified</span>
</a>`;
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
@@ -156,7 +156,7 @@ describe('diffs/components/commit_item', () => {
it('renders signature html', () => {
const actionsElement = getCommitActionsElement();
- const signatureElement = actionsElement.find('.gpg-status-box');
+ const signatureElement = actionsElement.find('.signature-badge');
expect(signatureElement.html()).toBe(TEST_SIGNATURE_HTML);
});
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index a6f508c73eb..6e9eb433924 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -21,262 +21,287 @@ function problemsClone({
};
}
-describe('isHighlighted', () => {
- it('should return true if line is highlighted', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = false;
- expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
- });
-
- it('should return false if line is not highlighted', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = false;
- expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
- });
-
- it('should return true if isCommented is true', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = true;
- expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
- });
-});
-
-describe('isContextLine', () => {
- it('return true if line type is context', () => {
- expect(utils.isContextLine(CONTEXT_LINE_TYPE)).toBe(true);
- });
-
- it('return false if line type is not context', () => {
- expect(utils.isContextLine('xxx')).toBe(false);
- });
-});
-
-describe('isMatchLine', () => {
- it('return true if line type is match', () => {
- expect(utils.isMatchLine(MATCH_LINE_TYPE)).toBe(true);
- });
-
- it('return false if line type is not match', () => {
- expect(utils.isMatchLine('xxx')).toBe(false);
- });
-});
-
-describe('isMetaLine', () => {
- it.each`
- type | expectation
- ${OLD_NO_NEW_LINE_TYPE} | ${true}
- ${NEW_NO_NEW_LINE_TYPE} | ${true}
- ${EMPTY_CELL_TYPE} | ${true}
- ${'xxx'} | ${false}
- `('should return $expectation if type is $type', ({ type, expectation }) => {
- expect(utils.isMetaLine(type)).toBe(expectation);
- });
-});
-
-describe('shouldRenderCommentButton', () => {
- it('should return false if comment button is not rendered', () => {
- expect(utils.shouldRenderCommentButton(true, false)).toBe(false);
- });
-
- it('should return false if not logged in', () => {
- expect(utils.shouldRenderCommentButton(false, true)).toBe(false);
- });
-
- it('should return true logged in and rendered', () => {
- expect(utils.shouldRenderCommentButton(true, true)).toBe(true);
- });
-});
-
-describe('hasDiscussions', () => {
- it('should return false if line is undefined', () => {
- expect(utils.hasDiscussions()).toBe(false);
- });
-
- it('should return false if discussions is undefined', () => {
- expect(utils.hasDiscussions({})).toBe(false);
- });
-
- it('should return false if discussions has legnth of 0', () => {
- expect(utils.hasDiscussions({ discussions: [] })).toBe(false);
- });
+describe('diff_row_utils', () => {
+ describe('isHighlighted', () => {
+ it('should return true if line is highlighted', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = false;
+ expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
+ });
+
+ it('should return false if line is not highlighted', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = false;
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
+ });
- it('should return true if discussions has legnth > 0', () => {
- expect(utils.hasDiscussions({ discussions: [1] })).toBe(true);
- });
-});
-
-describe('lineHref', () => {
- it(`should return #${LINE_CODE}`, () => {
- expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
- });
-
- it(`should return '#' if line is undefined`, () => {
- expect(utils.lineHref()).toEqual('#');
- });
-
- it(`should return '#' if line_code is undefined`, () => {
- expect(utils.lineHref({})).toEqual('#');
- });
-});
-
-describe('lineCode', () => {
- it(`should return undefined if line_code is undefined`, () => {
- expect(utils.lineCode()).toEqual(undefined);
- expect(utils.lineCode({ left: {} })).toEqual(undefined);
- expect(utils.lineCode({ right: {} })).toEqual(undefined);
- });
-
- it(`should return ${LINE_CODE}`, () => {
- expect(utils.lineCode({ line_code: LINE_CODE })).toEqual(LINE_CODE);
- expect(utils.lineCode({ left: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
- expect(utils.lineCode({ right: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
- });
-});
-
-describe('classNameMapCell', () => {
- it.each`
- line | hll | isLoggedIn | isHover | expectation
- ${undefined} | ${true} | ${true} | ${true} | ${[]}
- ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
- `('should return $expectation', ({ line, hll, isLoggedIn, isHover, expectation }) => {
- const classes = utils.classNameMapCell({ line, hll, isLoggedIn, isHover });
- expect(classes).toEqual(expectation);
- });
-});
-
-describe('addCommentTooltip', () => {
- const brokenSymLinkTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is not supported';
- const brokenRealTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is not supported';
- const lineMovedOrRenamedFileTooltip =
- 'Commenting on files that are only moved or renamed is not supported';
- const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
- const dragTooltip = 'Add a comment to this line or drag for multiple lines';
-
- it('should return default tooltip', () => {
- expect(utils.addCommentTooltip()).toBeUndefined();
- });
-
- it('should return drag comment tooltip when dragging is enabled', () => {
- expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
- });
-
- it('should return broken symlink tooltip', () => {
- expect(
- utils.addCommentTooltip({
- problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
- }),
- ).toEqual(brokenSymLinkTooltip);
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }) }),
- ).toEqual(brokenSymLinkTooltip);
- });
-
- it('should return broken real tooltip', () => {
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
- ).toEqual(brokenRealTooltip);
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
- ).toEqual(brokenRealTooltip);
- });
-
- it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
- expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
- lineMovedOrRenamedFileTooltip,
+ it('should return true if isCommented is true', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = true;
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
+ });
+ });
+
+ describe('isContextLine', () => {
+ it('return true if line type is context', () => {
+ expect(utils.isContextLine(CONTEXT_LINE_TYPE)).toBe(true);
+ });
+
+ it('return false if line type is not context', () => {
+ expect(utils.isContextLine('xxx')).toBe(false);
+ });
+ });
+
+ describe('isMatchLine', () => {
+ it('return true if line type is match', () => {
+ expect(utils.isMatchLine(MATCH_LINE_TYPE)).toBe(true);
+ });
+
+ it('return false if line type is not match', () => {
+ expect(utils.isMatchLine('xxx')).toBe(false);
+ });
+ });
+
+ describe('isMetaLine', () => {
+ it.each`
+ type | expectation
+ ${OLD_NO_NEW_LINE_TYPE} | ${true}
+ ${NEW_NO_NEW_LINE_TYPE} | ${true}
+ ${EMPTY_CELL_TYPE} | ${true}
+ ${'xxx'} | ${false}
+ `('should return $expectation if type is $type', ({ type, expectation }) => {
+ expect(utils.isMetaLine(type)).toBe(expectation);
+ });
+ });
+
+ describe('shouldRenderCommentButton', () => {
+ it('should return false if comment button is not rendered', () => {
+ expect(utils.shouldRenderCommentButton(true, false)).toBe(false);
+ });
+
+ it('should return false if not logged in', () => {
+ expect(utils.shouldRenderCommentButton(false, true)).toBe(false);
+ });
+
+ it('should return true logged in and rendered', () => {
+ expect(utils.shouldRenderCommentButton(true, true)).toBe(true);
+ });
+ });
+
+ describe('hasDiscussions', () => {
+ it('should return false if line is undefined', () => {
+ expect(utils.hasDiscussions()).toBe(false);
+ });
+
+ it('should return false if discussions is undefined', () => {
+ expect(utils.hasDiscussions({})).toBe(false);
+ });
+
+ it('should return false if discussions has legnth of 0', () => {
+ expect(utils.hasDiscussions({ discussions: [] })).toBe(false);
+ });
+
+ it('should return true if discussions has legnth > 0', () => {
+ expect(utils.hasDiscussions({ discussions: [1] })).toBe(true);
+ });
+ });
+
+ describe('lineHref', () => {
+ it(`should return #${LINE_CODE}`, () => {
+ expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
+ });
+
+ it(`should return '#' if line is undefined`, () => {
+ expect(utils.lineHref()).toEqual('#');
+ });
+
+ it(`should return '#' if line_code is undefined`, () => {
+ expect(utils.lineHref({})).toEqual('#');
+ });
+ });
+
+ describe('lineCode', () => {
+ it(`should return undefined if line_code is undefined`, () => {
+ expect(utils.lineCode()).toEqual(undefined);
+ expect(utils.lineCode({ left: {} })).toEqual(undefined);
+ expect(utils.lineCode({ right: {} })).toEqual(undefined);
+ });
+
+ it(`should return ${LINE_CODE}`, () => {
+ expect(utils.lineCode({ line_code: LINE_CODE })).toEqual(LINE_CODE);
+ expect(utils.lineCode({ left: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
+ expect(utils.lineCode({ right: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
+ });
+ });
+
+ describe('classNameMapCell', () => {
+ it.each`
+ line | highlighted | commented | selectionStart | selectionEnd | isLoggedIn | isHover | expectation
+ ${undefined} | ${true} | ${false} | ${false} | ${false} | ${true} | ${true} | ${[{ 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false }]}
+ ${undefined} | ${false} | ${true} | ${false} | ${false} | ${true} | ${true} | ${[{ 'highlight-top': false, 'highlight-bottom': false, hll: false, commented: true }]}
+ ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${false} | ${false} | ${false} | ${[{ new: true, 'highlight-top': false, 'highlight-bottom': false, hll: false, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${true} | ${false} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${false} | ${true} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${true} | ${true} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': true, new_line: true, old_line: false }]}
+ `(
+ 'should return $expectation',
+ ({
+ line,
+ highlighted,
+ commented,
+ selectionStart,
+ selectionEnd,
+ isLoggedIn,
+ isHover,
+ expectation,
+ }) => {
+ const classes = utils.classNameMapCell({
+ line,
+ highlighted,
+ commented,
+ selectionStart,
+ selectionEnd,
+ isLoggedIn,
+ isHover,
+ });
+ expect(classes).toEqual(expectation);
+ },
);
});
- it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
- expect(utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) })).toEqual(
- lineWithNoLineCodeTooltip,
+ describe('addCommentTooltip', () => {
+ const brokenSymLinkTooltip =
+ 'Commenting on symbolic links that replace or are replaced by files is not supported';
+ const brokenRealTooltip =
+ 'Commenting on files that replace or are replaced by symbolic links is not supported';
+ const lineMovedOrRenamedFileTooltip =
+ 'Commenting on files that are only moved or renamed is not supported';
+ const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
+ const dragTooltip = 'Add a comment to this line or drag for multiple lines';
+
+ it('should return default tooltip', () => {
+ expect(utils.addCommentTooltip()).toBeUndefined();
+ });
+
+ it('should return drag comment tooltip when dragging is enabled', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
+ });
+
+ it('should return broken symlink tooltip', () => {
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ });
+
+ it('should return broken real tooltip', () => {
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ });
+
+ it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
+ lineMovedOrRenamedFileTooltip,
+ );
+ });
+
+ it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) }),
+ ).toEqual(lineWithNoLineCodeTooltip);
+ });
+ });
+
+ describe('parallelViewLeftLineType', () => {
+ it(`should return ${OLD_NO_NEW_LINE_TYPE}`, () => {
+ expect(
+ utils.parallelViewLeftLineType({ line: { right: { type: NEW_NO_NEW_LINE_TYPE } } }),
+ ).toEqual(OLD_NO_NEW_LINE_TYPE);
+ });
+
+ it(`should return 'new'`, () => {
+ expect(utils.parallelViewLeftLineType({ line: { left: { type: 'new' } } })[0]).toBe('new');
+ });
+
+ it(`should return ${EMPTY_CELL_TYPE}`, () => {
+ expect(utils.parallelViewLeftLineType({})).toContain(EMPTY_CELL_TYPE);
+ });
+
+ it(`should return hll:true`, () => {
+ expect(utils.parallelViewLeftLineType({ highlighted: true })[1].hll).toBe(true);
+ });
+ });
+
+ describe('shouldShowCommentButton', () => {
+ it.each`
+ hover | context | meta | discussions | expectation
+ ${true} | ${false} | ${false} | ${false} | ${true}
+ ${false} | ${false} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${true} | ${false}
+ `(
+ 'should return $expectation when hover is $hover',
+ ({ hover, context, meta, discussions, expectation }) => {
+ expect(utils.shouldShowCommentButton(hover, context, meta, discussions)).toBe(expectation);
+ },
);
});
-});
-
-describe('parallelViewLeftLineType', () => {
- it(`should return ${OLD_NO_NEW_LINE_TYPE}`, () => {
- expect(utils.parallelViewLeftLineType({ right: { type: NEW_NO_NEW_LINE_TYPE } })).toEqual(
- OLD_NO_NEW_LINE_TYPE,
- );
- });
-
- it(`should return 'new'`, () => {
- expect(utils.parallelViewLeftLineType({ left: { type: 'new' } })).toContain('new');
- });
-
- it(`should return ${EMPTY_CELL_TYPE}`, () => {
- expect(utils.parallelViewLeftLineType({})).toContain(EMPTY_CELL_TYPE);
- });
-
- it(`should return hll:true`, () => {
- expect(utils.parallelViewLeftLineType({}, true)[1]).toEqual({ hll: true });
- });
-});
-
-describe('shouldShowCommentButton', () => {
- it.each`
- hover | context | meta | discussions | expectation
- ${true} | ${false} | ${false} | ${false} | ${true}
- ${false} | ${false} | ${false} | ${false} | ${false}
- ${true} | ${true} | ${false} | ${false} | ${false}
- ${true} | ${true} | ${true} | ${false} | ${false}
- ${true} | ${true} | ${true} | ${true} | ${false}
- `(
- 'should return $expectation when hover is $hover',
- ({ hover, context, meta, discussions, expectation }) => {
- expect(utils.shouldShowCommentButton(hover, context, meta, discussions)).toBe(expectation);
- },
- );
-});
-describe('mapParallel', () => {
- it('should assign computed properties to the line object', () => {
- const side = {
- discussions: [{}],
- discussionsExpanded: true,
- hasForm: true,
- problems: problemsClone(),
- };
- const content = {
- diffFile: {},
- hasParallelDraftLeft: () => false,
- hasParallelDraftRight: () => false,
- draftsForLine: () => [],
- };
- const line = { left: side, right: side };
- const expectation = {
- commentRowClasses: '',
- draftRowClasses: 'js-temp-notes-holder',
- hasDiscussionsLeft: true,
- hasDiscussionsRight: true,
- isContextLineLeft: false,
- isContextLineRight: false,
- isMatchLineLeft: false,
- isMatchLineRight: false,
- isMetaLineLeft: false,
- isMetaLineRight: false,
- };
- const leftExpectation = {
- renderDiscussion: true,
- hasDraft: false,
- lineDrafts: [],
- hasCommentForm: true,
- };
- const rightExpectation = {
- renderDiscussion: false,
- hasDraft: false,
- lineDrafts: [],
- hasCommentForm: false,
- };
- const mapped = utils.mapParallel(content)(line);
-
- expect(mapped).toMatchObject(expectation);
- expect(mapped.left).toMatchObject(leftExpectation);
- expect(mapped.right).toMatchObject(rightExpectation);
+ describe('mapParallel', () => {
+ it('should assign computed properties to the line object', () => {
+ const side = {
+ discussions: [{}],
+ discussionsExpanded: true,
+ hasForm: true,
+ problems: problemsClone(),
+ };
+ const content = {
+ diffFile: {},
+ hasParallelDraftLeft: () => false,
+ hasParallelDraftRight: () => false,
+ draftsForLine: () => [],
+ };
+ const line = { left: side, right: side };
+ const expectation = {
+ commentRowClasses: '',
+ draftRowClasses: 'js-temp-notes-holder',
+ hasDiscussionsLeft: true,
+ hasDiscussionsRight: true,
+ isContextLineLeft: false,
+ isContextLineRight: false,
+ isMatchLineLeft: false,
+ isMatchLineRight: false,
+ isMetaLineLeft: false,
+ isMetaLineRight: false,
+ };
+ const leftExpectation = {
+ renderDiscussion: true,
+ hasDraft: false,
+ lineDrafts: [],
+ hasCommentForm: true,
+ };
+ const rightExpectation = {
+ renderDiscussion: false,
+ hasDraft: false,
+ lineDrafts: [],
+ hasCommentForm: false,
+ };
+ const mapped = utils.mapParallel(content)(line);
+
+ expect(mapped).toMatchObject(expectation);
+ expect(mapped.left).toMatchObject(leftExpectation);
+ expect(mapped.right).toMatchObject(rightExpectation);
+ });
});
});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index ca7de8fd751..1656eaf8ba0 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -49,6 +49,7 @@ describe('Diffs tree list component', () => {
tempFile: true,
type: 'blob',
parentPath: 'app',
+ tree: [],
},
'test.rb': {
addedLines: 0,
@@ -62,6 +63,7 @@ describe('Diffs tree list component', () => {
tempFile: true,
type: 'blob',
parentPath: 'app',
+ tree: [],
},
app: {
key: 'app',
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 9e0ffbf757f..78765204322 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -16,6 +16,12 @@ import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
@@ -142,7 +148,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
),
)
- .reply(200, res1)
+ .reply(HTTP_STATUS_OK, res1)
.onGet(
mergeUrlParams(
{
@@ -154,7 +160,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
),
)
- .reply(200, res2);
+ .reply(HTTP_STATUS_OK, res2);
return testAction(
diffActions.fetchDiffFilesBatch,
@@ -186,7 +192,7 @@ describe('DiffsStoreActions', () => {
});
it('should fetch diff meta information', () => {
- mock.onGet(endpointMetadata).reply(200, diffMetadata);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_OK, diffMetadata);
return testAction(
diffActions.fetchDiffFilesMeta,
@@ -208,7 +214,7 @@ describe('DiffsStoreActions', () => {
});
it('should show a warning on 404 reponse', async () => {
- mock.onGet(endpointMetadata).reply(404);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND);
await testAction(
diffActions.fetchDiffFilesMeta,
@@ -228,7 +234,7 @@ describe('DiffsStoreActions', () => {
});
it('should show no warning on any other status code', async () => {
- mock.onGet(endpointMetadata).reply(500);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(
diffActions.fetchDiffFilesMeta,
@@ -248,7 +254,7 @@ describe('DiffsStoreActions', () => {
it('should commit SET_COVERAGE_DATA with received response', () => {
const data = { files: { 'app.js': { 1: 0, 2: 1 } } };
- mock.onGet(endpointCoverage).reply(200, { data });
+ mock.onGet(endpointCoverage).reply(HTTP_STATUS_OK, { data });
return testAction(
diffActions.fetchCoverageFiles,
@@ -260,7 +266,7 @@ describe('DiffsStoreActions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet(endpointCoverage).reply(400);
+ mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -545,7 +551,7 @@ describe('DiffsStoreActions', () => {
const nextLineNumbers = {};
const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers };
const contextLines = { contextLines: [{ lineCode: 6 }] };
- mock.onGet(endpoint).reply(200, contextLines);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, contextLines);
return testAction(
diffActions.loadMoreLines,
@@ -568,7 +574,7 @@ describe('DiffsStoreActions', () => {
const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
const commit = jest.fn();
- mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
+ mock.onGet(file.loadCollapsedDiffUrl).reply(HTTP_STATUS_OK, data);
return diffActions
.loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
@@ -1007,7 +1013,7 @@ describe('DiffsStoreActions', () => {
putSpy = jest.spyOn(axios, 'put');
gon = window.gon;
- mock.onPut(endpointUpdateUser).reply(200, {});
+ mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
@@ -1084,7 +1090,7 @@ describe('DiffsStoreActions', () => {
describe('fetchFullDiff', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/context`).replyOnce(200, ['test']);
+ mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_OK, ['test']);
});
it('commits the success and dispatches an action to expand the new lines', () => {
@@ -1105,7 +1111,7 @@ describe('DiffsStoreActions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/context`).replyOnce(500);
+ mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches receiveFullDiffError', () => {
@@ -1169,7 +1175,7 @@ describe('DiffsStoreActions', () => {
describe('success', () => {
beforeEach(() => {
renamedFile = { ...testFile, context_lines_path: SUCCESS_URL };
- mock.onGet(SUCCESS_URL).replyOnce(200, testData);
+ mock.onGet(SUCCESS_URL).replyOnce(HTTP_STATUS_OK, testData);
});
it.each`
@@ -1269,7 +1275,7 @@ describe('DiffsStoreActions', () => {
describe('setSuggestPopoverDismissed', () => {
it('commits SET_SHOW_SUGGEST_POPOVER', async () => {
const state = { dismissEndpoint: `${TEST_HOST}/-/user_callouts` };
- mock.onPost(state.dismissEndpoint).reply(200, {});
+ mock.onPost(state.dismissEndpoint).reply(HTTP_STATUS_OK, {});
jest.spyOn(axios, 'post');
@@ -1444,7 +1450,7 @@ describe('DiffsStoreActions', () => {
beforeEach(() => {
putSpy = jest.spyOn(axios, 'put');
- mock.onPut(updateUserEndpoint).reply(200, {});
+ mock.onPut(updateUserEndpoint).reply(HTTP_STATUS_OK, {});
});
it.each`
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 0f7926ccbf9..fdd157dd09f 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -7,7 +7,7 @@ import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
@@ -161,7 +161,7 @@ describe('dropzone_input', () => {
${'text/plain'} | ${TEST_ERROR_MESSAGE}
`('when AJAX fails with json', ({ responseType, responseBody }) => {
mock.post(TEST_UPLOAD_PATH, {
- status: 400,
+ status: HTTP_STATUS_BAD_REQUEST,
body: responseBody,
headers: { 'Content-Type': responseType },
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 19ebe0e3cb7..c42ac28c498 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -14,6 +14,7 @@ import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_edito
import SourceEditor from '~/editor/source_editor';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
@@ -154,7 +155,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
describe('onBeforeUnuse', () => {
beforeEach(async () => {
- mockAxios.onPost().reply(200, { body: responseData });
+ mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
await togglePreview();
});
afterEach(() => {
@@ -260,7 +261,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let previewMarkdownSpy;
beforeEach(() => {
- previewMarkdownSpy = jest.fn().mockImplementation(() => [200, { body: responseData }]);
+ previewMarkdownSpy = jest
+ .fn()
+ .mockImplementation(() => [HTTP_STATUS_OK, { body: responseData }]);
mockAxios.onPost(previewMarkdownPath).replyOnce((req) => previewMarkdownSpy(req));
});
@@ -285,7 +288,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
it('catches the errors when fetching the preview', async () => {
- mockAxios.onPost().reply(500);
+ mockAxios.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await fetchPreview();
expect(createAlert).toHaveBeenCalled();
@@ -321,7 +324,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
describe('togglePreview', () => {
beforeEach(() => {
- mockAxios.onPost().reply(200, { body: responseData });
+ mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
});
it('toggles the condition to toggle preview/hide actions in the context menu', () => {
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index cd3dfab30d4..3e9b49707ed 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('@sentry/browser');
jest.mock('~/vue_shared/plugins/global_toast');
@@ -41,10 +42,10 @@ describe('Awards app actions', () => {
window.gon = { relative_url_root: relativeRootUrl };
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
- .reply(200, ['thumbsup'], { 'x-next-page': '2' });
+ .reply(HTTP_STATUS_OK, ['thumbsup'], { 'x-next-page': '2' });
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
- .reply(200, ['thumbsdown']);
+ .reply(HTTP_STATUS_OK, ['thumbsdown']);
});
it('commits FETCH_AWARDS_SUCCESS', async () => {
@@ -61,7 +62,7 @@ describe('Awards app actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet('/awards').reply(500);
+ mock.onGet('/awards').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
@@ -115,7 +116,7 @@ describe('Awards app actions', () => {
describe('adding new award', () => {
describe('success', () => {
beforeEach(() => {
- mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_OK, { id: 1 });
});
it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
@@ -129,7 +130,7 @@ describe('Awards app actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onPost(`${relativeRootUrl || ''}/awards`).reply(500);
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
@@ -152,7 +153,7 @@ describe('Awards app actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(200);
+ mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(HTTP_STATUS_OK);
});
it('commits REMOVE_AWARD', async () => {
@@ -174,7 +175,9 @@ describe('Awards app actions', () => {
const name = 'thumbsup';
beforeEach(() => {
- mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
+ mock
+ .onDelete(`${relativeRootUrl || ''}/awards/1`)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index c005ca22070..73a366457fb 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -1,6 +1,6 @@
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import { deployBoardMockData } from './mock_data';
@@ -10,7 +10,7 @@ describe('Deploy Board', () => {
let wrapper;
const createComponent = (props = {}) =>
- mount(Vue.extend(DeployBoard), {
+ mount(DeployBoard, {
propsData: {
deployBoardData: deployBoardMockData,
isLoading: false,
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 5ea23af4c16..fb1a8b8c00a 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
@@ -14,6 +15,7 @@ const DEFAULT_OPTS = {
provide: {
projectEnvironmentsPath: '/projects/environments',
updateEnvironmentPath: '/proejcts/environments/1',
+ protectedEnvironmentSettingsPath: '/projects/1/settings/ci_cd',
},
propsData: { environment: { id: '0', name: 'foo', external_url: 'https://foo.example.com' } },
};
@@ -67,7 +69,7 @@ describe('~/environments/components/edit.vue', () => {
expect(showsLoading()).toBe(false);
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@@ -75,7 +77,7 @@ describe('~/environments/components/edit.vue', () => {
it('submits the updated environment on submit', async () => {
const expected = { url: 'https://google.ca' };
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@@ -83,7 +85,7 @@ describe('~/environments/components/edit.vue', () => {
it('shows errors on error', async () => {
const expected = { url: 'https://google.ca' };
- await submitForm(expected, [400, { message: ['uh oh!'] }]);
+ await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]);
expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
diff --git a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
new file mode 100644
index 00000000000..725c8c6479e
--- /dev/null
+++ b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
@@ -0,0 +1,47 @@
+import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ActionsComponent from '~/environments/components/environment_actions.vue';
+
+describe('~/environments/environment_details/components/deployment_actions.vue', () => {
+ let wrapper;
+
+ const actionsData = [
+ {
+ playable: true,
+ playPath: 'http://www.example.com/play',
+ name: 'deploy-staging',
+ scheduledAt: '2023-01-18T08:50:08.390Z',
+ },
+ ];
+
+ const createWrapper = ({ actions }) => {
+ return mountExtended(DeploymentActions, {
+ propsData: {
+ actions,
+ },
+ });
+ };
+
+ describe('when there is no actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: [] });
+ });
+
+ it('should not render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(false);
+ });
+ });
+
+ describe('when there are actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: actionsData });
+ });
+
+ it('should render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(true);
+ expect(actionsComponent.props().actions).toBe(actionsData);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/deployment_job_spec.js b/spec/frontend/environments/environment_details/components/deployment_job_spec.js
index 9bb61abb293..9bb61abb293 100644
--- a/spec/frontend/environments/environment_details/deployment_job_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_job_spec.js
diff --git a/spec/frontend/environments/environment_details/deployment_status_link_spec.js b/spec/frontend/environments/environment_details/components/deployment_status_link_spec.js
index 5db7740423a..5db7740423a 100644
--- a/spec/frontend/environments/environment_details/deployment_status_link_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_status_link_spec.js
diff --git a/spec/frontend/environments/environment_details/deployment_triggerer_spec.js b/spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js
index 48af82661bf..48af82661bf 100644
--- a/spec/frontend/environments/environment_details/deployment_triggerer_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index f1af08bcf32..b9b34bee80f 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -10,11 +10,14 @@ const DEFAULT_PROPS = {
cancelPath: '/cancel',
};
+const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd' };
+
describe('~/environments/components/form.vue', () => {
let wrapper;
const createWrapper = (propsData = {}) =>
mountExtended(EnvironmentForm, {
+ provide: PROVIDE,
propsData: {
...DEFAULT_PROPS,
...propsData,
@@ -31,7 +34,7 @@ describe('~/environments/components/form.vue', () => {
});
it('links to documentation regarding environments', () => {
- const link = wrapper.findByRole('link', { name: 'More information' });
+ const link = wrapper.findByRole('link', { name: 'More information.' });
expect(link.attributes('href')).toBe('/help/ci/environments/index.md');
});
@@ -124,6 +127,10 @@ describe('~/environments/components/form.vue', () => {
expect(urlInput.element.value).toBe('');
});
+
+ it('does not show protected environment documentation', () => {
+ expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false);
+ });
});
describe('when an existing environment is being edited', () => {
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 65a9f2907d2..986ecca4e84 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -195,6 +195,36 @@ describe('~/environments/components/environments_app.vue', () => {
expect(button.exists()).toBe(false);
});
+ it('should not show a button to clean up environments if the user has no permissions', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ canStopStaleEnvironments: false,
+ },
+ folder: resolvedFolder,
+ });
+
+ const button = wrapper.findByRole('button', {
+ name: s__('Environments|Clean up environments'),
+ });
+ expect(button.exists()).toBe(false);
+ });
+
+ it('should show a button to clean up environments if the user has permissions', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ canStopStaleEnvironments: true,
+ },
+ folder: resolvedFolder,
+ });
+
+ const button = wrapper.findByRole('button', {
+ name: s__('Environments|Clean up environments'),
+ });
+ expect(button.exists()).toBe(true);
+ });
+
describe('tabs', () => {
it('should show tabs for available and stopped environmets', async () => {
await createWrapperWithMocked({
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index 72a7449f24e..a87060f83d8 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { environmentsList } from './mock_data';
describe('Environments Folder View', () => {
@@ -29,7 +30,7 @@ describe('Environments Folder View', () => {
describe('successful request', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
- 200,
+ HTTP_STATUS_OK,
{
environments: environmentsList,
stopped_count: 1,
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index f8b8465cf6f..23506eb018d 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -5,6 +5,7 @@ import { removeBreakLine, removeWhitespace } from 'helpers/text_helper';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
@@ -22,7 +23,7 @@ describe('Environments Folder View', () => {
const mockEnvironments = (environmentList) => {
mock.onGet(mockData.endpoint).reply(
- 200,
+ HTTP_STATUS_OK,
{
environments: environmentList,
stopped_count: 1,
@@ -54,7 +55,6 @@ describe('Environments Folder View', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('successful request', () => {
@@ -95,32 +95,12 @@ describe('Environments Folder View', () => {
it('should render pagination', () => {
expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
-
- it('should make an API request when changing page', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.find('.gl-pagination .page-item:nth-last-of-type(2) .page-link').trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: wrapper.vm.scope,
- page: '10',
- nested: true,
- });
- });
-
- it('should make an API request when using tabs', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- findEnvironmentsTabStopped().trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: 'stopped',
- page: '1',
- nested: true,
- });
- });
});
});
describe('unsuccessfull request', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(500, { environments: [] });
+ mock.onGet(mockData.endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { environments: [] });
createWrapper();
return axios.waitForAll();
});
@@ -160,29 +140,5 @@ describe('Environments Folder View', () => {
expect(wrapper.vm.requestData.page).toEqual('4');
}));
});
-
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.vm.onChangeTab('stopped');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: 'stopped',
- page: '1',
- nested: true,
- });
- });
- });
-
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.vm.onChangePage(4);
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: wrapper.vm.scope,
- page: '4',
- nested: true,
- });
- });
- });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 355b77b55c3..5ea0be41614 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -166,7 +166,7 @@ export const environmentsApp = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/911/play',
method: 'post',
- button_title: 'Trigger this manual action',
+ button_title: 'Run job',
},
},
},
@@ -265,6 +265,7 @@ export const environmentsApp = {
review_snippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
},
+ can_stop_stale_environments: true,
available_count: 4,
stopped_count: 0,
};
@@ -373,7 +374,7 @@ export const resolvedEnvironmentsApp = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/911/play',
method: 'post',
- buttonTitle: 'Trigger this manual action',
+ buttonTitle: 'Run job',
},
},
},
@@ -474,6 +475,7 @@ export const resolvedEnvironmentsApp = {
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
__typename: 'ReviewApp',
},
+ canStopStaleEnvironments: true,
stoppedCount: 0,
__typename: 'LocalEnvironmentApp',
};
@@ -673,7 +675,7 @@ export const resolvedEnvironment = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
method: 'post',
- buttonTitle: 'Trigger this manual action',
+ buttonTitle: 'Run job',
},
},
},
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 7684cca2303..2c223d3a1a7 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
@@ -44,7 +45,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const search = '';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search } })
- .reply(200, environmentsApp, {});
+ .reply(HTTP_STATUS_OK, environmentsApp, {});
const app = await mockResolvers.Query.environmentApp(
null,
@@ -63,7 +64,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const interval = 3000;
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {
+ .reply(HTTP_STATUS_OK, environmentsApp, {
'poll-interval': interval,
});
@@ -78,7 +79,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {
+ .reply(HTTP_STATUS_OK, environmentsApp, {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
@@ -108,7 +109,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {});
+ .reply(HTTP_STATUS_OK, environmentsApp, {});
await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
@@ -131,7 +132,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should fetch the folder url passed to it', async () => {
mock
.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '' } })
- .reply(200, folder);
+ .reply(HTTP_STATUS_OK, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
@@ -144,7 +145,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('stopEnvironment', () => {
it('should post to the stop environment path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
@@ -161,7 +162,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
it('should set is stopping to false if stop fails', async () => {
- mock.onPost(ENDPOINT).reply(500);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
@@ -180,7 +181,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('rollbackEnvironment', () => {
it('should post to the retry environment path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.rollbackEnvironment(null, {
environment: { retryUrl: ENDPOINT },
@@ -193,7 +194,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('deleteEnvironment', () => {
it('should DELETE to the delete environment path', async () => {
- mock.onDelete(ENDPOINT).reply(200);
+ mock.onDelete(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.deleteEnvironment(null, {
environment: { deletePath: ENDPOINT },
@@ -206,7 +207,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('cancelAutoStop', () => {
it('should post to the auto stop path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.cancelAutoStop(null, { autoStopUrl: ENDPOINT });
@@ -262,13 +263,13 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('action', () => {
it('should POST to the given path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
});
it('should return a nice error message on fail', async () => {
- mock.onPost(ENDPOINT).reply(500);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({
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
index 401c10338c1..326a28bd769 100644
--- 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
@@ -2,6 +2,14 @@
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 1`] = `
Object {
+ "actions": Array [
+ Object {
+ "name": "deploy-staging",
+ "playPath": "https://gdk.test:3000/redeploy/play",
+ "playable": true,
+ "scheduledAt": "2023-01-17T11:02:41.369Z",
+ },
+ ],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -35,6 +43,7 @@ Object {
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 2`] = `
Object {
+ "actions": Array [],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -65,6 +74,7 @@ Object {
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 3`] = `
Object {
+ "actions": Array [],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
diff --git a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
index 8bb87c0a208..65bb804a58e 100644
--- a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
+++ b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
@@ -23,7 +23,7 @@ describe('deployment_data_transformation_helper', () => {
},
};
- const commitWithourAuthor = {
+ const commitWithoutAuthor = {
id: 'gid://gitlab/CommitPresenter/02274a949a88c9aef68a29685d99bd9a661a7f9b',
shortId: '02274a94',
message: 'Commit message',
@@ -48,6 +48,24 @@ describe('deployment_data_transformation_helper', () => {
refName: 'main',
id: 'gid://gitlab/Ci::Build/860',
webPath: '/gitlab-org/pipelinestest/-/jobs/860',
+ deploymentPipeline: {
+ jobs: {
+ nodes: [
+ {
+ name: 'deploy-staging',
+ playable: true,
+ scheduledAt: '2023-01-17T11:02:41.369Z',
+ webPath: 'https://gdk.test:3000/redeploy',
+ },
+ {
+ name: 'deploy-production',
+ playable: true,
+ scheduledAt: '2023-01-17T11:02:41.369Z',
+ webPath: 'https://gdk.test:3000/redeploy',
+ },
+ ],
+ },
+ },
},
commit: commitWithAuthor,
triggerer: {
@@ -65,8 +83,15 @@ describe('deployment_data_transformation_helper', () => {
finishedAt: null,
};
+ const environment = {
+ lastDeployment: {
+ job: {
+ name: 'deploy-production',
+ },
+ },
+ };
describe('getAuthorFromCommit', () => {
- it.each([commitWithAuthor, commitWithourAuthor])('should be properly converted', (commit) => {
+ it.each([commitWithAuthor, commitWithoutAuthor])('should be properly converted', (commit) => {
expect(getAuthorFromCommit(commit)).toMatchSnapshot();
});
});
@@ -89,7 +114,7 @@ describe('deployment_data_transformation_helper', () => {
it.each([deploymentNode, deploymentNodeWithEmptyJob, deploymentNodeWithNoJob])(
'should be converted to proper table row data',
(node) => {
- expect(convertToDeploymentTableRow(node)).toMatchSnapshot();
+ expect(convertToDeploymentTableRow(node, environment)).toMatchSnapshot();
},
);
});
diff --git a/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js b/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js
new file mode 100644
index 00000000000..b624178e3db
--- /dev/null
+++ b/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js
@@ -0,0 +1,69 @@
+import { shallowMount } from '@vue/test-utils';
+import environmentsPaginationApiMixin from '~/environments/mixins/environments_pagination_api_mixin';
+
+describe('environments_pagination_api_mixin', () => {
+ const updateContentMock = jest.fn();
+ const mockComponent = {
+ template: `
+ <div>
+ <button id='change-page' @click="changePageClick" />
+ <button id='change-tab' @click="changeTabClick" />
+ </div>
+ `,
+ methods: {
+ updateContent: updateContentMock,
+ changePageClick() {
+ this.onChangePage(this.nextPage);
+ },
+ changeTabClick() {
+ this.onChangeTab(this.nextScope);
+ },
+ },
+ data() {
+ return {
+ scope: 'test',
+ };
+ },
+ };
+
+ let wrapper;
+
+ const createWrapper = ({ scope, nextPage, nextScope }) =>
+ shallowMount(mockComponent, {
+ mixins: [environmentsPaginationApiMixin],
+ data() {
+ return {
+ nextPage,
+ nextScope,
+ scope,
+ };
+ },
+ });
+
+ it.each([
+ ['test-scope', 2],
+ ['test-scope', 10],
+ ['test-scope-2', 3],
+ ])('should call updateContent when calling onChangePage', async (scopeName, pageNumber) => {
+ wrapper = createWrapper({ scope: scopeName, nextPage: pageNumber });
+
+ await wrapper.find('#change-page').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({
+ scope: scopeName,
+ page: pageNumber.toString(),
+ nested: true,
+ });
+ });
+
+ it('should call updateContent when calling onChageTab', async () => {
+ wrapper = createWrapper({ nextScope: 'stopped' });
+ await wrapper.find('#change-tab').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({
+ scope: 'stopped',
+ page: '1',
+ nested: true,
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 6dd4eea7437..a8cc05b297b 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -5,13 +5,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
const DEFAULT_OPTS = {
- provide: { projectEnvironmentsPath: '/projects/environments' },
+ provide: {
+ projectEnvironmentsPath: '/projects/environments',
+ protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd',
+ },
};
describe('~/environments/components/new.vue', () => {
@@ -76,7 +80,7 @@ describe('~/environments/components/new.vue', () => {
expect(showsLoading()).toBe(false);
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@@ -84,7 +88,7 @@ describe('~/environments/components/new.vue', () => {
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@@ -92,7 +96,7 @@ describe('~/environments/components/new.vue', () => {
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
- await submitForm(expected, [400, { message: ['name taken'] }]);
+ await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] }]);
expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
new file mode 100644
index 00000000000..a2ab4f707b5
--- /dev/null
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -0,0 +1,60 @@
+import MockAdapter from 'axios-mock-adapter';
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StopStaleEnvironmentsModal from '~/environments/components/stop_stale_environments_modal.vue';
+import axios from '~/lib/utils/axios_utils';
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+import { STOP_STALE_ENVIRONMENTS_PATH } from '~/api/environments_api';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+
+const DEFAULT_OPTS = {
+ provide: { projectId: 1 },
+};
+
+const ONE_WEEK_AGO = getDateInPast(new Date(), 7);
+const TEN_YEARS_AGO = getDateInPast(new Date(), 3650);
+
+describe('~/environments/components/stop_stale_environments_modal.vue', () => {
+ let wrapper;
+ let mock;
+ let before;
+ let originalGon;
+
+ const createWrapper = (opts = {}) =>
+ shallowMount(StopStaleEnvironmentsModal, {
+ ...DEFAULT_OPTS,
+ ...opts,
+ propsData: { modalId: 'stop-stale-environments-modal', visible: true },
+ });
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { api_version: 'v4' };
+
+ mock = new MockAdapter(axios);
+ jest.spyOn(axios, 'post');
+ wrapper = createWrapper();
+ before = wrapper.find("[data-testid='stop-environments-before']");
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ jest.resetAllMocks();
+ window.gon = originalGon;
+ });
+
+ it('sets the correct min and max dates', async () => {
+ expect(before.props().minDate.toISOString()).toBe(TEN_YEARS_AGO.toISOString());
+ expect(before.props().maxDate.toISOString()).toBe(ONE_WEEK_AGO.toISOString());
+ });
+
+ it('requests cleanup when submit is clicked', async () => {
+ mock.onPost().replyOnce(HTTP_STATUS_OK);
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+ const url = STOP_STALE_ENVIRONMENTS_PATH.replace(':id', 1).replace(':version', 'v4');
+ expect(axios.post).toHaveBeenCalledWith(url, null, {
+ params: { before: ONE_WEEK_AGO.toISOString() },
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 8f085282f80..3ec43010d80 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/flash.js');
@@ -29,7 +30,7 @@ describe('Sentry common store actions', () => {
describe('updateStatus', () => {
it('should handle successful status update', async () => {
- mock.onPut().reply(200, {});
+ mock.onPut().reply(HTTP_STATUS_OK, {});
await testAction(
actions.updateStatus,
params,
@@ -46,7 +47,7 @@ describe('Sentry common store actions', () => {
});
it('should handle unsuccessful status update', async () => {
- mock.onPut().reply(400, {});
+ mock.onPut().reply(HTTP_STATUS_BAD_REQUEST, {});
await testAction(actions.updateStatus, params, {}, [], []);
expect(visitUrl).not.toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 1893d226270..383d8aaeb20 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -4,6 +4,11 @@ import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
let mockedAdapter;
@@ -30,7 +35,7 @@ describe('Sentry error details store actions', () => {
const endpoint = '123/stacktrace';
it('should commit SET_ERROR with received response', () => {
const payload = { error: [1, 2, 3] };
- mockedAdapter.onGet().reply(200, payload);
+ mockedAdapter.onGet().reply(HTTP_STATUS_OK, payload);
return testAction(
actions.startPollingStacktrace,
{ endpoint },
@@ -44,7 +49,7 @@ describe('Sentry error details store actions', () => {
});
it('should show flash on API error', async () => {
- mockedAdapter.onGet().reply(400);
+ mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.startPollingStacktrace,
@@ -58,7 +63,7 @@ describe('Sentry error details store actions', () => {
it('should not restart polling when receiving an empty 204 response', async () => {
mockedRestart = jest.spyOn(Poll.prototype, 'restart');
- mockedAdapter.onGet().reply(204);
+ mockedAdapter.onGet().reply(HTTP_STATUS_NO_CONTENT);
await testAction(actions.startPollingStacktrace, { endpoint }, {}, [], []);
mockedRestart = jest.spyOn(Poll.prototype, 'restart');
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index bcd816c2ae0..d8f61be6df7 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -6,6 +6,7 @@ import * as types from '~/error_tracking_settings/store/mutation_types';
import defaultState from '~/error_tracking_settings/store/state';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { projectList } from '../mock';
@@ -28,7 +29,7 @@ describe('error tracking settings actions', () => {
});
it('should request and transform the project list', async () => {
- mock.onGet(TEST_HOST).reply(() => [200, { projects: projectList }]);
+ mock.onGet(TEST_HOST).reply(() => [HTTP_STATUS_OK, { projects: projectList }]);
await testAction(
actions.fetchProjects,
null,
@@ -46,7 +47,7 @@ describe('error tracking settings actions', () => {
});
it('should handle a server error', async () => {
- mock.onGet(`${TEST_HOST}.json`).reply(() => [400]);
+ mock.onGet(`${TEST_HOST}.json`).reply(() => [HTTP_STATUS_BAD_REQUEST]);
await testAction(
actions.fetchProjects,
null,
@@ -118,14 +119,14 @@ describe('error tracking settings actions', () => {
});
it('should save the page', async () => {
- mock.onPatch(TEST_HOST).reply(200);
+ mock.onPatch(TEST_HOST).reply(HTTP_STATUS_OK);
await testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }]);
expect(mock.history.patch.length).toBe(1);
expect(refreshCurrentPage).toHaveBeenCalled();
});
it('should handle a server error', async () => {
- mock.onPatch(TEST_HOST).reply(400);
+ mock.onPatch(TEST_HOST).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.updateSettings,
null,
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index 05709cd05e6..cf4605e21ea 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -10,6 +10,7 @@ import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
import createStore from '~/feature_flags/store/edit';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
Vue.use(Vuex);
@@ -35,7 +36,7 @@ describe('Edit feature flag form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(200, {
+ mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(HTTP_STATUS_OK, {
id: 21,
iid: 5,
active: true,
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index d27b23c5cd1..e80f9c559c4 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -11,6 +11,7 @@ import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue'
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData } from '../mock_data';
@@ -74,7 +75,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory(provideData);
return waitForPromises();
});
@@ -119,7 +120,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory(provideData);
return waitForPromises();
});
@@ -141,7 +142,7 @@ describe('Feature flags', () => {
it('renders a loading icon', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .replyOnce(200, getRequestData, {});
+ .replyOnce(HTTP_STATUS_OK, getRequestData, {});
factory();
@@ -158,7 +159,7 @@ describe('Feature flags', () => {
beforeEach(async () => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
feature_flags: [],
count: {
@@ -203,14 +204,16 @@ describe('Feature flags', () => {
describe('with paginated feature flags', () => {
beforeEach(() => {
- mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(200, getRequestData, {
- 'x-next-page': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '5',
- });
+ mock
+ .onGet(mockState.endpoint, { params: { page: '1' } })
+ .replyOnce(HTTP_STATUS_OK, getRequestData, {
+ 'x-next-page': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '5',
+ });
factory();
jest.spyOn(store, 'dispatch');
@@ -271,7 +274,9 @@ describe('Feature flags', () => {
describe('unsuccessful request', () => {
beforeEach(() => {
- mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(500, {});
+ mock
+ .onGet(mockState.endpoint, { params: { page: '1' } })
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
factory();
return waitForPromises();
@@ -303,7 +308,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory();
return waitForPromises();
});
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index b71cdf78207..14e1f34bc59 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -49,7 +49,7 @@ describe('New Environments Dropdown', () => {
describe('with empty results', () => {
let item;
beforeEach(async () => {
- axiosMock.onGet(TEST_HOST).reply(200, []);
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
await axios.waitForAll();
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index 7132e83a940..8b9b42f4eb1 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -17,6 +17,7 @@ import * as types from '~/feature_flags/store/edit/mutation_types';
import state from '~/feature_flags/store/edit/state';
import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/url_utility');
@@ -55,7 +56,9 @@ describe('Feature flags Edit Module actions', () => {
},
],
};
- mock.onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag)).replyOnce(200);
+ mock
+ .onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag))
+ .replyOnce(HTTP_STATUS_OK);
return testAction(
updateFeatureFlag,
@@ -76,7 +79,9 @@ describe('Feature flags Edit Module actions', () => {
describe('error', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError', () => {
- mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] });
+ mock
+ .onPut(`${TEST_HOST}/endpoint.json`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: [] });
return testAction(
updateFeatureFlag,
@@ -155,7 +160,7 @@ describe('Feature flags Edit Module actions', () => {
describe('success', () => {
it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 });
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, { id: 1 });
return testAction(
fetchFeatureFlag,
@@ -177,7 +182,9 @@ describe('Feature flags Edit Module actions', () => {
describe('error', () => {
it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
fetchFeatureFlag,
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index 96a7d868316..46a7843b937 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -20,6 +20,7 @@ import {
import * as types from '~/feature_flags/store/index/mutation_types';
import state from '~/feature_flags/store/index/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
jest.mock('~/api.js');
@@ -57,7 +58,7 @@ describe('Feature flags actions', () => {
describe('success', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {});
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, getRequestData, {});
return testAction(
fetchFeatureFlags,
@@ -79,7 +80,9 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
fetchFeatureFlags,
@@ -154,7 +157,7 @@ describe('Feature flags actions', () => {
describe('success', () => {
it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess', () => {
- mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {});
+ mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, rotateData, {});
return testAction(
rotateInstanceId,
@@ -176,7 +179,9 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
rotateInstanceId,
@@ -252,7 +257,7 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
- mock.onPut(featureFlag.update_path).replyOnce(200, featureFlag, {});
+ mock.onPut(featureFlag.update_path).replyOnce(HTTP_STATUS_OK, featureFlag, {});
return testAction(
toggleFeatureFlag,
@@ -275,7 +280,7 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
- mock.onPut(featureFlag.update_path).replyOnce(500);
+ mock.onPut(featureFlag.update_path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
toggleFeatureFlag,
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index dbe6669c868..01b6ab4d5ed 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -11,6 +11,7 @@ import {
import * as types from '~/feature_flags/store/new/mutation_types';
import state from '~/feature_flags/store/new/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/url_utility');
@@ -48,7 +49,9 @@ describe('Feature flags New Module Actions', () => {
},
],
};
- mock.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams)).replyOnce(200);
+ mock
+ .onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
+ .replyOnce(HTTP_STATUS_OK);
return testAction(
createFeatureFlag,
@@ -85,7 +88,7 @@ describe('Feature flags New Module Actions', () => {
};
mock
.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
- .replyOnce(500, { message: [] });
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: [] });
return testAction(
createFeatureFlag,
diff --git a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
index dff6d11a320..30e1bfe94b5 100644
--- a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -2,13 +2,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Filtered Search Dropdown Manager', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
});
describe('addWordToInput', () => {
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 28fcf0b7ec7..ec0c712f959 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 { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Filtered Search Visual Tokens', () => {
@@ -24,7 +25,7 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
setHTMLFixture(`
<ul class="tokens-container">
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 43c10090739..d3fa8fae9ab 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -79,7 +79,7 @@ describe('Filtered Search Visual Tokens', () => {
it('replaces author token with avatar and display name', async () => {
const dummyUser = {
name: 'Important Person',
- avatar_url: 'https://host.invalid/mypics/avatar.png',
+ avatar_url: `${TEST_HOST}/mypics/avatar.png`,
};
const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index ac58b99875b..6d452bf1bff 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -39,6 +39,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) }
+ let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) }
let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) }
let!(:pending) { create(:ci_build, :pending, name: 'pending', pipeline: pipeline) }
let!(:playable) { create(:ci_build, :playable, name: 'playable', pipeline: pipeline) }
diff --git a/spec/frontend/fixtures/listbox.rb b/spec/frontend/fixtures/listbox.rb
index 8f8489a2827..8f746f1707a 100644
--- a/spec/frontend/fixtures/listbox.rb
+++ b/spec/frontend/fixtures/listbox.rb
@@ -26,6 +26,9 @@ RSpec.describe 'initRedirectListboxBehavior', '(JavaScript fixtures)', type: :he
arbitrary_key: 'qux xyz'
}]
- @tag = helper.gl_redirect_listbox_tag(items, 'bar', class: %w[test-class-1 test-class-2], data: { right: true })
+ @tag = helper.gl_redirect_listbox_tag(items, 'bar',
+ class: %w[test-class-1 test-class-2],
+ data: { placement: 'right' }
+ )
end
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 18f89fbc5e5..7ee89ca3694 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -148,6 +148,53 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
end
end
+ context 'merge request with no approvals' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}_no_approvals.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'merge request approved by current user' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ merge_request.approved_by_users << user
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'merge request approved by multiple users' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}_multiple_users.json" do
+ merge_request.approved_by_users << user
+ merge_request.approved_by_users << create(:user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
context 'merge request in state getState query' do
base_input_path = 'vue_merge_request_widget/queries/'
base_output_path = 'graphql/merge_requests/'
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 44b471a70d8..768934d6278 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -23,8 +23,19 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') }
let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') }
+ let(:bridge) { create(:ci_bridge, pipeline: pipeline) }
+ let(:retried_bridge) { create(:ci_bridge, :retried, pipeline: pipeline) }
+
+ let(:downstream_pipeline) { create(:ci_pipeline, :with_job) }
+ let(:retried_downstream_pipeline) { create(:ci_pipeline, :with_job) }
+ let!(:ci_sources_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
+ let!(:retried_ci_sources_pipeline) do
+ create(:ci_sources_pipeline, pipeline: retried_downstream_pipeline, source_job: retried_bridge)
+ end
+
before do
sign_in(user)
+ project.add_developer(user)
end
it 'pipelines/pipelines.json' do
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index de87114766e..f60e4991292 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
before do
allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
allow(instance).to receive(:check_runner_upgrade_suggestion)
- .and_return([nil, :not_available])
+ .and_return([nil, :unavailable])
end
end
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb
new file mode 100644
index 00000000000..c80ba06bca1
--- /dev/null
+++ b/spec/frontend/fixtures/saved_replies.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do
+ include JavaScriptFixturesHelpers
+ include ApiHelpers
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'when user has no saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies_empty.query.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user has saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies.query.graphql.json" do
+ create(:saved_reply, user: current_user)
+ create(:saved_reply, user: current_user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/static/project_select_combo_button.html b/spec/frontend/fixtures/static/project_select_combo_button.html
deleted file mode 100644
index 3776610ed4c..00000000000
--- a/spec/frontend/fixtures/static/project_select_combo_button.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<div class="project-item-select-holder">
- <input class="project-item-select" data-group-id="12345" data-relative-path="issues/new" />
- <a class="js-new-project-item-link" data-label="issue" data-type="issues" href="">
- <span class="gl-spinner"></span>
- </a>
- <a class="new-project-item-select-button">
- <svg data-testid="chevron-down-icon" class="gl-icon s16">
- <use
- href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#chevron-down"
- ></use>
- </svg>
- </a>
-</div>
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 2f0a52a9884..17d6cea23df 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,12 +1,6 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import {
- hideFlash,
- addDismissFlashClickListener,
- FLASH_CLOSED_EVENT,
- createAlert,
- VARIANT_WARNING,
-} from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
jest.mock('@sentry/browser');
@@ -14,65 +8,6 @@ describe('Flash', () => {
const findTextContent = (containerSelector = '.flash-container') =>
document.querySelector(containerSelector).textContent.replace(/\s+/g, ' ').trim();
- describe('hideFlash', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.className = 'js-testing';
- });
-
- it('sets transition style', () => {
- hideFlash(el);
-
- expect(el.style.transition).toBe('opacity 0.15s');
- });
-
- it('sets opacity style', () => {
- hideFlash(el);
-
- expect(el.style.opacity).toBe('0');
- });
-
- it('does not set styles when fadeTransition is false', () => {
- hideFlash(el, false);
-
- expect(el.style.opacity).toBe('');
- expect(el.style.transition).toHaveLength(0);
- });
-
- it('removes element after transitionend', () => {
- document.body.appendChild(el);
-
- hideFlash(el);
- el.dispatchEvent(new Event('transitionend'));
-
- expect(document.querySelector('.js-testing')).toBeNull();
- });
-
- it('calls event listener callback once', () => {
- jest.spyOn(el, 'remove');
- document.body.appendChild(el);
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.remove.mock.calls.length).toBe(1);
- });
-
- it(`dispatches ${FLASH_CLOSED_EVENT} event after transitionend event`, () => {
- jest.spyOn(el, 'dispatchEvent');
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.dispatchEvent).toHaveBeenCalledWith(new Event(FLASH_CLOSED_EVENT));
- });
- });
-
describe('createAlert', () => {
const mockMessage = 'a message';
let alert;
@@ -338,45 +273,4 @@ describe('Flash', () => {
});
});
});
-
- describe('addDismissFlashClickListener', () => {
- let el;
-
- describe('with close icon', () => {
- beforeEach(() => {
- el = document.createElement('div');
- el.innerHTML = `
- <div class="flash-container">
- <div class="flash">
- <div class="close-icon js-close-icon"></div>
- </div>
- </div>
- `;
- });
-
- it('removes global flash on click', () => {
- addDismissFlashClickListener(el, false);
-
- el.querySelector('.js-close-icon').click();
-
- expect(document.querySelector('.flash')).toBeNull();
- });
- });
-
- describe('without close icon', () => {
- beforeEach(() => {
- el = document.createElement('div');
- el.innerHTML = `
- <div class="flash-container">
- <div class="flash">
- </div>
- </div>
- `;
- });
-
- it('does not throw', () => {
- expect(() => addDismissFlashClickListener(el, false)).not.toThrow();
- });
- });
- });
});
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index b1e87aca63d..e1890555de0 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -12,6 +12,7 @@ import eventHub from '~/frequent_items/event_hub';
import { createStore } from '~/frequent_items/store';
import { getTopFrequentItems } from '~/frequent_items/utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
Vue.use(Vuex);
@@ -32,6 +33,7 @@ describe('Frequent Items App Component', () => {
const createComponent = (props = {}) => {
const session = currentSession[TEST_NAMESPACE];
gon.api_version = session.apiVersion;
+ gon.features = { fullPathProjectSearch: true };
wrapper = mountExtended(App, {
store,
@@ -115,7 +117,9 @@ describe('Frequent Items App Component', () => {
});
it('should render searched projects list', async () => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects.data);
+ mock
+ .onGet(/\/api\/v4\/projects.json(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, mockSearchedProjects.data);
setSearch('gitlab');
await nextTick();
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 4f2badf869d..c54a2a1d039 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -154,7 +154,8 @@ describe('FrequentItemsListItemComponent', () => {
link.vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
- label: 'projects_dropdown_frequent_items_list_item_git_lab_community_edition',
+ label: 'projects_dropdown_frequent_items_list_item',
+ property: 'navigation_top',
});
});
});
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index 94fc97b82c2..dfce88ca0a8 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -65,6 +65,7 @@ describe('FrequentItemsSearchInputComponent', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'type_search_query', {
label: 'projects_dropdown_frequent_items_search_input',
+ property: 'navigation_top',
});
expect(store.dispatch).toHaveBeenCalledWith('frequentProjects/setSearchQuery', value);
});
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index 4f998cc26da..c228bca4973 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as types from '~/frequent_items/store/mutation_types';
import state from '~/frequent_items/store/state';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockNamespace,
@@ -24,6 +25,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
+ gon.features = { fullPathProjectSearch: true };
});
afterEach(() => {
@@ -173,7 +175,9 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
it('should dispatch `receiveSearchedItemsSuccess`', () => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
+ mock
+ .onGet(/\/api\/v4\/projects.json(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, mockSearchedProjects, {});
return testAction(
actions.fetchSearchedItems,
@@ -192,7 +196,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
it('should dispatch `receiveSearchedItemsError`', () => {
gon.api_version = 'v4';
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.fetchSearchedItems,
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index cc2dc084e47..e4fd8649263 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -17,6 +17,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
eventlistenersMockDefaultMap,
crmContactsMock,
@@ -184,17 +185,20 @@ describe('GfmAutoComplete', () => {
});
});
- it.each([200, 500])('should set the loading state', async (responseStatus) => {
- mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
+ it.each([HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR])(
+ 'should set the loading state',
+ async (responseStatus) => {
+ mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
- fetchData.call(context, {}, '[vulnerability:', 'query');
+ fetchData.call(context, {}, '[vulnerability:', 'query');
- expect(context.isLoadingData['[vulnerability:']).toBe(true);
+ expect(context.isLoadingData['[vulnerability:']).toBe(true);
- await waitForPromises();
+ await waitForPromises();
- expect(context.isLoadingData['[vulnerability:']).toBe(false);
- });
+ expect(context.isLoadingData['[vulnerability:']).toBe(false);
+ },
+ );
});
describe('data is in cache', () => {
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index ab5627ce216..6ad9d9f4338 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -6,6 +6,47 @@ import '~/lib/utils/common_utils';
describe('GLForm', () => {
const testContext = {};
+ const mockGl = {
+ GfmAutoComplete: {
+ dataSources: {
+ commands: '/group/projects/-/autocomplete_sources/commands',
+ },
+ },
+ };
+
+ describe('Setting up GfmAutoComplete', () => {
+ describe('setupForm', () => {
+ let setupFormSpy;
+
+ beforeEach(() => {
+ setupFormSpy = jest.spyOn(GLForm.prototype, 'setupForm');
+
+ testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ testContext.textarea = testContext.form.find('textarea');
+ });
+
+ it('should be called with the global data source `windows.gl`', () => {
+ window.gl = { ...mockGl };
+ testContext.glForm = new GLForm(testContext.form, {}, false);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(window.gl.GfmAutoComplete.dataSources, false);
+ });
+
+ it('should be called with the provided custom data source', () => {
+ window.gl = { ...mockGl };
+
+ const customDataSources = {
+ foobar: '/group/projects/-/autocomplete_sources/foobar',
+ };
+
+ testContext.glForm = new GLForm(testContext.form, {}, false, customDataSources);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(customDataSources, false);
+ });
+ });
+ });
describe('when instantiated', () => {
beforeEach(() => {
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 0a1596b492d..2d961e6872e 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -3,6 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import GpgBadges from '~/gpg_badges';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('GpgBadges', () => {
let mock;
@@ -27,7 +28,7 @@ describe('GpgBadges', () => {
<input type="search" name="search" value="${search}" id="commits-search"class="form-control search-text-input input-short">
</form>
<div class="parent-container">
- <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div>
+ <div class="js-loading-signature-badge" data-commit-sha="${dummyCommitSha}"></div>
</div>
`);
};
@@ -63,7 +64,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
await GpgBadges.fetch();
@@ -75,7 +76,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures with search parameters with spaces', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
setForm({ search: 'my search' });
await GpgBadges.fetch();
@@ -88,7 +89,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures with search parameters with plus symbols', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
setForm({ search: 'my+search' });
await GpgBadges.fetch();
@@ -101,20 +102,20 @@ describe('GpgBadges', () => {
});
it('displays a loading spinner', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
await GpgBadges.fetch();
- expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
- const spinners = document.querySelectorAll('.js-loading-gpg-badge span.gl-spinner');
+ expect(document.querySelector('.js-loading-signature-badge:empty')).toBe(null);
+ const spinners = document.querySelectorAll('.js-loading-signature-badge span.gl-spinner');
expect(spinners.length).toBe(1);
});
it('replaces the loading spinner', async () => {
- mock.onGet(dummyUrl).replyOnce(200, dummyResponse);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK, dummyResponse);
await GpgBadges.fetch();
- expect(document.querySelector('.js-loading-gpg-badge')).toBe(null);
+ expect(document.querySelector('.js-loading-signature-badge')).toBe(null);
const parentContainer = document.querySelector('.parent-container');
expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml);
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index bf899e47d1c..cd334ef0d97 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
convertToGraphQLIds,
convertFromGraphQLIds,
convertNodeIdsFromGraphQLIds,
+ getNodesOrDefault,
} from '~/graphql_shared/utils';
const mockType = 'Group';
@@ -134,3 +135,28 @@ describe('convertNodeIdsFromGraphQLIds', () => {
);
});
});
+
+describe('getNodesOrDefault', () => {
+ const mockDataWithNodes = {
+ users: {
+ nodes: [
+ { __typename: 'UserCore', id: 'gid://gitlab/User/44' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/42' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/41' },
+ ],
+ },
+ };
+
+ it.each`
+ desc | input | expected
+ ${'with nodes child'} | ${[mockDataWithNodes.users]} | ${mockDataWithNodes.users.nodes}
+ ${'with nodes child and "dne" as field'} | ${[mockDataWithNodes.users, 'dne']} | ${[]}
+ ${'with empty data object'} | ${[{ users: {} }]} | ${[]}
+ ${'with empty object'} | ${[{}]} | ${[]}
+ ${'with falsy value'} | ${[undefined]} | ${[]}
+ `('$desc', ({ input, expected }) => {
+ const result = getNodesOrDefault(...input);
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 140609161d4..4e6ddd89a55 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -11,6 +11,12 @@ import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -66,7 +72,7 @@ describe('AppComponent', () => {
beforeEach(async () => {
mock = new AxiosMockAdapter(axios);
- mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockGroups);
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
setWindowLocation('?filter=foobar');
@@ -101,7 +107,7 @@ describe('AppComponent', () => {
});
it('should set headers to store for building pagination info when called with `updatePagination`', () => {
- mock.onGet('/dashboard/groups.json').reply(200, { headers: mockRawPageInfo });
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, { headers: mockRawPageInfo });
jest.spyOn(vm, 'updatePagination').mockImplementation(() => {});
@@ -112,7 +118,7 @@ describe('AppComponent', () => {
});
it('should show flash error when request fails', () => {
- mock.onGet('/dashboard/groups.json').reply(400);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
return vm.fetchGroups({}).then(() => {
@@ -145,7 +151,7 @@ describe('AppComponent', () => {
});
it('should fetch matching set of groups when app is loaded with search query', () => {
- mock.onGet('/dashboard/groups.json').reply(200, mockSearchedGroups);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockSearchedGroups);
const fetchPromise = vm.fetchAllGroups();
@@ -216,7 +222,7 @@ describe('AppComponent', () => {
});
it('should fetch children of given group and expand it if group is collapsed and children are not loaded', () => {
- mock.onGet('/dashboard/groups.json').reply(200, mockRawChildren);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockRawChildren);
jest.spyOn(vm, 'fetchGroups');
jest.spyOn(vm.store, 'setGroupChildren').mockImplementation(() => {});
@@ -252,7 +258,7 @@ describe('AppComponent', () => {
});
it('should set `isChildrenLoading` back to `false` if load request fails', () => {
- mock.onGet('/dashboard/groups.json').reply(400);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
vm.toggleChildren(groupItem);
@@ -321,7 +327,9 @@ describe('AppComponent', () => {
it('should show error flash message if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
- jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
+ jest
+ .spyOn(vm.service, 'leaveGroup')
+ .mockRejectedValue({ status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
jest.spyOn(vm.store, 'removeGroup');
vm.leaveGroup();
@@ -336,7 +344,7 @@ describe('AppComponent', () => {
it('should show appropriate error flash message if request forbids to leave group', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
- jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
+ jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');
vm.leaveGroup(childGroupItem, groupItem);
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index d25b45bd662..4a385cb00ee 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -6,6 +6,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/common_utils');
@@ -89,7 +90,7 @@ describe('InviteMembersBanner', () => {
it('sends the dismissEvent when the banner is dismissed', () => {
mockTrackingOnWrapper();
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ mockAxios.onPost(provide.calloutsPath).replyOnce(HTTP_STATUS_OK);
const dismissEvent = 'invite_members_banner_dismissed';
wrapper.findComponent(GlBanner).vm.$emit('close');
@@ -136,7 +137,7 @@ describe('InviteMembersBanner', () => {
});
it('should close the banner when dismiss is clicked', async () => {
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ mockAxios.onPost(provide.calloutsPath).replyOnce(HTTP_STATUS_OK);
expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
wrapper.findComponent(GlBanner).vm.$emit('close');
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
index 74424ee3230..77c0966ba1e 100644
--- a/spec/frontend/groups_projects/components/transfer_locations_spec.js
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -15,7 +15,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { __ } from '~/locale';
-import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import TransferLocations, { i18n } from '~/groups_projects/components/transfer_locations.vue';
import { getTransferLocations } from '~/api/projects_api';
import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
@@ -374,4 +374,23 @@ describe('TransferLocations', () => {
expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true);
});
});
+
+ describe('when there are no results', () => {
+ it('displays no results message', async () => {
+ mockResolvedGetTransferLocations({
+ data: [],
+ page: '1',
+ nextPage: null,
+ total: '0',
+ totalPages: '1',
+ prevPage: null,
+ });
+
+ createComponent({ propsData: { showUserTransferLocations: false } });
+
+ await showDropdown();
+
+ expect(wrapper.findComponent(GlDropdownItem).text()).toBe(i18n.NO_RESULTS_TEXT);
+ });
+ });
});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index c714c269ca0..d6263c663d2 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -375,7 +375,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
label: 'global_search',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
@@ -388,7 +388,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
label: 'global_search',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index 1ae149128ca..bd93b0edadf 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/header_search/store/actions';
import * as types from '~/header_search/store/mutation_types';
import initState from '~/header_search/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
MOCK_SEARCH,
MOCK_AUTOCOMPLETE_OPTIONS_RES,
@@ -37,9 +38,9 @@ describe('Header Search Store Actions', () => {
});
describe.each`
- axiosMock | type | expectedMutations
- ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
- ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
+ axiosMock | type | expectedMutations
+ ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
+ ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
describe(`on ${type}`, () => {
beforeEach(() => {
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
new file mode 100644
index 00000000000..8dd3745e0ac
--- /dev/null
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -0,0 +1,61 @@
+import { createWrapper } from '@vue/test-utils';
+import Vue from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+
+const MockComponent = Vue.component('MockComponent', {
+ props: {
+ someKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ count: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ render: (createElement) => createElement('span'),
+});
+
+let wrapper;
+
+const findMock = () => wrapper.findComponent(MockComponent);
+
+const didCreateApp = () => wrapper !== undefined;
+
+const initMock = (html, props = {}) => {
+ setHTMLFixture(html);
+
+ const app = initSimpleApp('#mount-here', MockComponent, { props });
+
+ wrapper = app ? createWrapper(app) : undefined;
+};
+
+describe('helpers/init_simple_app_helper/initSimpleApp', () => {
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('mounts the component if the selector exists', async () => {
+ initMock('<div id="mount-here"></div>');
+
+ expect(findMock().exists()).toBe(true);
+ });
+
+ it('does not mount the component if selector does not exist', async () => {
+ initMock('<div id="do-not-mount-here"></div>');
+
+ expect(didCreateApp()).toBe(false);
+ });
+
+ it('passes the prop to the component if the prop exists', async () => {
+ initMock(`<div id="mount-here" data-view-model={"someKey":"thing","count":123}></div>`);
+
+ expect(findMock().props()).toEqual({
+ someKey: 'thing',
+ count: 123,
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 294f5eee863..1d81c3ea89d 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -68,31 +68,6 @@ describe('ide/components/panes/right.vue', () => {
});
});
- describe('clientside live preview tab', () => {
- it('is shown if there is a packageJson and clientsidePreviewEnabled', () => {
- Vue.set(store.state.entries, 'package.json', {
- name: 'package.json',
- });
- store.state.clientsidePreviewEnabled = true;
-
- createComponent();
-
- expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: true,
- title: 'Live preview',
- views: expect.arrayContaining([
- expect.objectContaining({
- name: rightSidebarViews.clientSidePreview.name,
- }),
- ]),
- }),
- ]),
- );
- });
- });
-
describe('terminal tab', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
deleted file mode 100644
index 51e6a9d9034..00000000000
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ /dev/null
@@ -1,416 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import { dispatch } from 'codesandbox-api';
-import { SandpackClient } from '@codesandbox/sandpack-client';
-import Vuex from 'vuex';
-import waitForPromises from 'helpers/wait_for_promises';
-import Clientside from '~/ide/components/preview/clientside.vue';
-import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
-import eventHub from '~/ide/eventhub';
-
-jest.mock('@codesandbox/sandpack-client', () => ({
- SandpackClient: jest.fn(),
-}));
-
-Vue.use(Vuex);
-
-const dummyPackageJson = () => ({
- raw: JSON.stringify({
- main: 'index.js',
- }),
-});
-const expectedSandpackOptions = () => ({
- files: {},
- entry: '/index.js',
- showOpenInCodeSandbox: true,
-});
-const expectedSandpackSettings = () => ({
- fileResolver: {
- isFile: expect.any(Function),
- readFile: expect.any(Function),
- },
-});
-
-describe('IDE clientside preview', () => {
- let wrapper;
- let store;
- const storeActions = {
- getFileData: jest.fn().mockReturnValue(Promise.resolve({})),
- getRawFileData: jest.fn().mockReturnValue(Promise.resolve('')),
- };
- const storeClientsideActions = {
- pingUsage: jest.fn().mockReturnValue(Promise.resolve({})),
- };
- const dispatchCodesandboxReady = () => dispatch({ type: 'done' });
-
- const createComponent = ({ state, getters } = {}) => {
- store = new Vuex.Store({
- state: {
- entries: {},
- links: {},
- ...state,
- },
- getters: {
- packageJson: () => '',
- currentProject: () => ({
- visibility: 'public',
- }),
- ...getters,
- },
- actions: storeActions,
- modules: {
- clientside: {
- namespaced: true,
- actions: storeClientsideActions,
- },
- },
- });
-
- wrapper = shallowMount(Clientside, {
- store,
- });
- };
-
- const createInitializedComponent = () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- sandpackReady: true,
- client: {
- cleanup: jest.fn(),
- updatePreview: jest.fn(),
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('without main entry', () => {
- it('creates sandpack client', () => {
- createComponent();
- expect(SandpackClient).not.toHaveBeenCalled();
- });
- });
- describe('with main entry', () => {
- beforeEach(() => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
-
- return waitForPromises();
- });
-
- it('creates sandpack client', () => {
- expect(SandpackClient).toHaveBeenCalledWith(
- '#ide-preview',
- expectedSandpackOptions(),
- expectedSandpackSettings(),
- );
- });
-
- it('pings usage', () => {
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(1);
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
- expect.anything(),
- PING_USAGE_PREVIEW_KEY,
- );
- });
-
- it('pings usage success', async () => {
- dispatchCodesandboxReady();
- await nextTick();
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(2);
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
- expect.anything(),
- PING_USAGE_PREVIEW_SUCCESS_KEY,
- );
- });
- });
-
- describe('with codesandboxBundlerUrl', () => {
- const TEST_BUNDLER_URL = 'https://test.gitlab-static.test';
-
- beforeEach(() => {
- createComponent({
- getters: { packageJson: dummyPackageJson },
- state: { codesandboxBundlerUrl: TEST_BUNDLER_URL },
- });
-
- return waitForPromises();
- });
-
- it('creates sandpack client with bundlerURL', () => {
- expect(SandpackClient).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
- ...expectedSandpackSettings(),
- bundlerURL: TEST_BUNDLER_URL,
- });
- });
- });
-
- describe('with codesandboxBundlerURL', () => {
- beforeEach(() => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
-
- return waitForPromises();
- });
-
- it('creates sandpack client', () => {
- expect(SandpackClient).toHaveBeenCalledWith(
- '#ide-preview',
- {
- files: {},
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- },
- {
- fileResolver: {
- isFile: expect.any(Function),
- readFile: expect.any(Function),
- },
- },
- );
- });
- });
-
- describe('computed', () => {
- describe('normalizedEntries', () => {
- it('returns flattened list of blobs with content', () => {
- createComponent({
- state: {
- entries: {
- 'index.js': { type: 'blob', raw: 'test' },
- 'index2.js': { type: 'blob', content: 'content' },
- tree: { type: 'tree' },
- empty: { type: 'blob' },
- },
- },
- });
-
- expect(wrapper.vm.normalizedEntries).toEqual({
- '/index.js': {
- code: 'test',
- },
- '/index2.js': {
- code: 'content',
- },
- });
- });
- });
-
- describe('mainEntry', () => {
- it('returns false when package.json is empty', () => {
- createComponent();
- expect(wrapper.vm.mainEntry).toBe(false);
- });
-
- it('returns main key from package.json', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- expect(wrapper.vm.mainEntry).toBe('index.js');
- });
- });
-
- describe('showPreview', () => {
- it('returns false if no mainEntry', () => {
- createComponent();
- expect(wrapper.vm.showPreview).toBe(false);
- });
-
- it('returns false if loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- expect(wrapper.vm.showPreview).toBe(false);
- });
-
- it('returns true if not loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- expect(wrapper.vm.showPreview).toBe(true);
- });
- });
-
- describe('showEmptyState', () => {
- it('returns true if no mainEntry exists', () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
- expect(wrapper.vm.showEmptyState).toBe(true);
- });
-
- it('returns false if loading', () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- expect(wrapper.vm.showEmptyState).toBe(false);
- });
-
- it('returns false if not loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- expect(wrapper.vm.showEmptyState).toBe(false);
- });
- });
-
- describe('showOpenInCodeSandbox', () => {
- it('returns true when visibility is public', () => {
- createComponent({ getters: { currentProject: () => ({ visibility: 'public' }) } });
-
- expect(wrapper.vm.showOpenInCodeSandbox).toBe(true);
- });
-
- it('returns false when visibility is private', () => {
- createComponent({ getters: { currentProject: () => ({ visibility: 'private' }) } });
-
- expect(wrapper.vm.showOpenInCodeSandbox).toBe(false);
- });
- });
-
- describe('sandboxOpts', () => {
- beforeEach(() => {
- createComponent({
- state: {
- entries: {
- 'index.js': { type: 'blob', raw: 'test' },
- 'package.json': dummyPackageJson(),
- },
- },
- getters: {
- packageJson: dummyPackageJson,
- },
- });
- });
-
- it('returns sandbox options', () => {
- expect(wrapper.vm.sandboxOpts).toEqual({
- files: {
- '/index.js': {
- code: 'test',
- },
- '/package.json': {
- code: '{"main":"index.js"}',
- },
- },
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('loadFileContent', () => {
- beforeEach(() => {
- createComponent();
- return wrapper.vm.loadFileContent('package.json');
- });
-
- it('calls getFileData', () => {
- expect(storeActions.getFileData).toHaveBeenCalledWith(expect.any(Object), {
- path: 'package.json',
- makeFileActive: false,
- });
- });
-
- it('calls getRawFileData', () => {
- expect(storeActions.getRawFileData).toHaveBeenCalledWith(expect.any(Object), {
- path: 'package.json',
- });
- });
- });
-
- describe('update', () => {
- it('initializes client if client is empty', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ sandpackReady: true });
- wrapper.vm.update();
-
- return waitForPromises().then(() => {
- expect(SandpackClient).toHaveBeenCalled();
- });
- });
-
- it('calls updatePreview', () => {
- createInitializedComponent();
-
- wrapper.vm.update();
-
- expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
- });
- });
-
- describe('on ide.files.change event', () => {
- beforeEach(() => {
- createInitializedComponent();
-
- eventHub.$emit('ide.files.change');
- });
-
- it('calls updatePreview', () => {
- expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
- });
- });
- });
-
- describe('template', () => {
- it('renders ide-preview element when showPreview is true', async () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- await nextTick();
- expect(wrapper.find('#ide-preview').exists()).toBe(true);
- });
-
- it('renders empty state', async () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- await nextTick();
- expect(wrapper.text()).toContain(
- 'Preview your web application using Web IDE client-side evaluation.',
- );
- });
-
- it('renders loading icon', async () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
-
- describe('when destroyed', () => {
- let spy;
-
- beforeEach(() => {
- createInitializedComponent();
- spy = wrapper.vm.client.updatePreview;
- wrapper.destroy();
- });
-
- it('does not call updatePreview', () => {
- expect(spy).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
deleted file mode 100644
index 043dcade858..00000000000
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { listen } from 'codesandbox-api';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
-
-jest.mock('codesandbox-api', () => ({
- listen: jest.fn().mockReturnValue(jest.fn()),
-}));
-
-describe('IDE clientside preview navigator', () => {
- let wrapper;
- let client;
- let listenHandler;
-
- const findBackButton = () => wrapper.findAll('button').at(0);
- const findForwardButton = () => wrapper.findAll('button').at(1);
- const findRefreshButton = () => wrapper.findAll('button').at(2);
-
- beforeEach(() => {
- listen.mockClear();
- client = { bundlerURL: TEST_HOST, iframe: { src: '' } };
-
- wrapper = shallowMount(ClientsideNavigator, { propsData: { client } });
- [[listenHandler]] = listen.mock.calls;
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders readonly URL bar', async () => {
- listenHandler({ type: 'urlchange', url: client.bundlerURL });
- await nextTick();
- expect(wrapper.find('input[readonly]').element.value).toBe('/');
- });
-
- it('renders loading icon by default', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
- it('removes loading icon when done event is fired', async () => {
- listenHandler({ type: 'done' });
- await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- });
-
- it('does not count visiting same url multiple times', async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
- await nextTick();
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('unsubscribes from listen on destroy', () => {
- const unsubscribeFn = listen();
-
- wrapper.destroy();
- expect(unsubscribeFn).toHaveBeenCalled();
- });
-
- describe('back button', () => {
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url: TEST_HOST });
- await nextTick();
- });
-
- it('is disabled by default', () => {
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('is enabled when there is previous entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- await nextTick();
- findBackButton().trigger('click');
- expect(findBackButton().attributes()).not.toHaveProperty('disabled');
- });
-
- it('is disabled when there is no previous entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('updates client iframe src', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- await nextTick();
- findBackButton().trigger('click');
-
- expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
- });
-
- describe('forward button', () => {
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url: TEST_HOST });
- await nextTick();
- });
-
- it('is disabled by default', () => {
- expect(findForwardButton().attributes('disabled')).toBe('disabled');
- });
-
- it('is enabled when there is next entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- expect(findForwardButton().attributes()).not.toHaveProperty('disabled');
- });
-
- it('is disabled when there is no next entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- findForwardButton().trigger('click');
-
- await nextTick();
- expect(findForwardButton().attributes('disabled')).toBe('disabled');
- });
-
- it('updates client iframe src', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- await nextTick();
- findBackButton().trigger('click');
-
- expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
- });
-
- describe('refresh button', () => {
- const url = `${TEST_HOST}/some_url`;
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url });
- await nextTick();
- });
-
- it('calls refresh with current path', () => {
- client.iframe.src = 'something-other';
- findRefreshButton().trigger('click');
-
- expect(client.iframe.src).toBe(url);
- });
- });
-});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9092d73571b..c9f033bffbb 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -22,6 +22,7 @@ import ModelManager from '~/ide/lib/common/model_manager';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import SourceEditorInstance from '~/editor/source_editor_instance';
import { file } from '../helpers';
@@ -265,7 +266,7 @@ describe('RepoEditor', () => {
mock = new MockAdapter(axios);
- mock.onPost(/(.*)\/preview_markdown/).reply(200, {
+ mock.onPost(/(.*)\/preview_markdown/).reply(HTTP_STATUS_OK, {
body: `<p>${dummyFile.text.content}</p>`,
});
});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 97254ab680b..bfc87f17092 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -3,6 +3,7 @@ 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 { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
import { TEST_HOST } from 'helpers/test_constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -32,6 +33,9 @@ const TEST_START_REMOTE_PARAMS = {
remotePath: '/test/projects/f oo',
connectionToken: '123abc',
};
+const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/jetbrains-mono/JetBrainsMono.woff2';
+const TEST_EDITOR_FONT_FORMAT = 'woff2';
+const TEST_EDITOR_FONT_FAMILY = 'JebBrains Mono';
describe('ide/init_gitlab_web_ide', () => {
let resolveConfirm;
@@ -49,6 +53,9 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
el.dataset.mergeRequest = TEST_MR_ID;
el.dataset.filePath = TEST_FILE_PATH;
+ el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
+ el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
+ el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
document.body.append(el);
};
@@ -103,7 +110,13 @@ describe('ide/init_gitlab_web_ide', () => {
userPreferences: TEST_USER_PREFERENCES_PATH,
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
},
+ editorFont: {
+ srcUrl: TEST_EDITOR_FONT_SRC_URL,
+ fontFamily: TEST_EDITOR_FONT_FAMILY,
+ format: TEST_EDITOR_FONT_FORMAT,
+ },
handleStartRemote: expect.any(Function),
+ handleTracking,
});
});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js
new file mode 100644
index 00000000000..5dff9b6f118
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js
@@ -0,0 +1,32 @@
+import { snakeCase } from 'lodash';
+import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
+import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
+import { mockTracking } from 'helpers/tracking_helper';
+
+describe('ide/handle_tracking_event', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ });
+
+ describe('when the event does not contain data', () => {
+ it('does not send extra property to snowplow', () => {
+ const event = { name: 'event-name' };
+
+ handleTracking(event);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, snakeCase(event.name));
+ });
+ });
+
+ describe('when the event contains data', () => {
+ it('sends extra property to snowplow', () => {
+ const event = { name: 'event-name', data: { 'extra-details': 'details' } };
+
+ handleTracking(event);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, snakeCase(event.name), {
+ extra: convertObjectPropsToSnakeCase(event.data),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/mirror_spec.js b/spec/frontend/ide/lib/mirror_spec.js
index 8f417ea54dc..98e6b0deee6 100644
--- a/spec/frontend/ide/lib/mirror_spec.js
+++ b/spec/frontend/ide/lib/mirror_spec.js
@@ -7,6 +7,7 @@ import {
MSG_CONNECTION_ERROR,
SERVICE_DELAY,
} from '~/ide/lib/mirror';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getWebSocketUrl } from '~/lib/utils/url_utility';
jest.mock('~/ide/lib/create_diff', () => jest.fn());
@@ -18,15 +19,18 @@ const TEST_DIFF = {
};
const TEST_ERROR = 'Something bad happened...';
const TEST_SUCCESS_RESPONSE = {
- data: JSON.stringify({ error: { code: 0 }, payload: { status_code: 200 } }),
+ data: JSON.stringify({ error: { code: 0 }, payload: { status_code: HTTP_STATUS_OK } }),
};
const TEST_ERROR_RESPONSE = {
- data: JSON.stringify({ error: { code: 1, Message: TEST_ERROR }, payload: { status_code: 200 } }),
+ data: JSON.stringify({
+ error: { code: 1, Message: TEST_ERROR },
+ payload: { status_code: HTTP_STATUS_OK },
+ }),
};
const TEST_ERROR_PAYLOAD_RESPONSE = {
data: JSON.stringify({
error: { code: 0 },
- payload: { status_code: 500, error_message: TEST_ERROR },
+ payload: { status_code: HTTP_STATUS_INTERNAL_SERVER_ERROR, error_message: TEST_ERROR },
}),
};
diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js
index 0f23b0a4e45..413e7b2e4b7 100644
--- a/spec/frontend/ide/remote/index_spec.js
+++ b/spec/frontend/ide/remote/index_spec.js
@@ -3,6 +3,7 @@ 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';
+import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
jest.mock('@gitlab/web-ide');
jest.mock('~/ide/lib/gitlab_web_ide');
@@ -24,7 +25,6 @@ const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`;
describe('~/ide/remote/index', () => {
useMockLocationHelper();
const originalHref = window.location.href;
-
let el;
let rootEl;
@@ -56,6 +56,7 @@ describe('~/ide/remote/index', () => {
hostPath: `/${TEST_DATA.remotePath}`,
handleError: expect.any(Function),
handleClose: expect.any(Function),
+ handleTracking,
});
});
});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 5847e8e1518..623dee387e5 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -5,6 +5,7 @@ import Api from '~/api';
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import services from '~/ide/services';
import { query, mutate } from '~/ide/services/gql';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { projectData } from '../mock_data';
@@ -108,7 +109,7 @@ describe('IDE services', () => {
};
mock = new MockAdapter(axios);
- mock.onGet(file.rawPath).reply(200, 'raw content');
+ mock.onGet(file.rawPath).reply(HTTP_STATUS_OK, 'raw content');
jest.spyOn(axios, 'get');
});
@@ -205,7 +206,7 @@ describe('IDE services', () => {
filePath,
)}`,
)
- .reply(200, TEST_FILE_CONTENTS);
+ .reply(HTTP_STATUS_OK, TEST_FILE_CONTENTS);
});
it('fetches file content', () =>
@@ -230,7 +231,7 @@ describe('IDE services', () => {
mock
.onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`)
- .reply(200, [TEST_FILE_PATH]);
+ .reply(HTTP_STATUS_OK, [TEST_FILE_PATH]);
});
afterEach(() => {
@@ -271,7 +272,7 @@ describe('IDE services', () => {
const TEST_PROJECT_PATH = 'foo/bar';
const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/service_ping/web_ide_pipelines_count`;
- mock.onPost(axiosURL).reply(200);
+ mock.onPost(axiosURL).reply(HTTP_STATUS_OK);
return services.pingUsage(TEST_PROJECT_PATH).then(() => {
expect(axios.post).toHaveBeenCalledWith(axiosURL);
diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js
index 788fdb6471c..5f752197e13 100644
--- a/spec/frontend/ide/services/terminals_spec.js
+++ b/spec/frontend/ide/services/terminals_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import * as terminalService from '~/ide/services/terminals';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/ipsum/dolar';
const TEST_BRANCH = 'ref';
@@ -11,7 +12,7 @@ describe('~/ide/services/terminals', () => {
const prevRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
- axiosSpy = jest.fn().mockReturnValue([200, {}]);
+ axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]);
mock = new MockAdapter(axios);
mock.onPost(/.*/).reply((...args) => axiosSpy(...args));
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 38a54e569a9..90ca8526698 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -7,6 +7,7 @@ import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { file, createTriggerRenameAction, createTriggerUpdatePayload } from '../../helpers';
@@ -243,7 +244,7 @@ describe('IDE store file actions', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).replyOnce(
- 200,
+ HTTP_STATUS_OK,
{
raw_path: 'raw_path',
},
@@ -320,7 +321,7 @@ describe('IDE store file actions', () => {
store.state.entries[localFile.path] = localFile;
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/old-dull-file`).replyOnce(
- 200,
+ HTTP_STATUS_OK,
{
raw_path: 'raw_path',
},
@@ -377,7 +378,7 @@ describe('IDE store file actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/(.*)/).replyOnce(200, 'raw');
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, 'raw');
});
it('calls getRawFileData service method', () => {
@@ -470,7 +471,7 @@ describe('IDE store file actions', () => {
describe('return JSON', () => {
beforeEach(() => {
- mock.onGet(/(.*)/).replyOnce(200, JSON.stringify({ test: '123' }));
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, JSON.stringify({ test: '123' }));
});
it('does not parse returned JSON', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index f1b2a7b881a..fbae84631ee 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/ide/stores/actions/merge_request';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;
@@ -63,7 +64,9 @@ describe('IDE store merge request actions', () => {
describe('base case', () => {
beforeEach(() => {
jest.spyOn(service, 'getProjectMergeRequests');
- mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/)
+ .reply(HTTP_STATUS_OK, mockData);
});
it('calls getProjectMergeRequests service method', async () => {
@@ -113,7 +116,7 @@ describe('IDE store merge request actions', () => {
describe('no merge requests for branch available case', () => {
beforeEach(() => {
jest.spyOn(service, 'getProjectMergeRequests');
- mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(HTTP_STATUS_OK, []);
});
it('does not fail if there are no merge requests for current branch', async () => {
@@ -155,7 +158,7 @@ describe('IDE store merge request actions', () => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
- .reply(200, { title: 'mergerequest' });
+ .reply(HTTP_STATUS_OK, { title: 'mergerequest' });
});
it('calls getProjectMergeRequestData service method', async () => {
@@ -212,7 +215,7 @@ describe('IDE store merge request actions', () => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
- .reply(200, { title: 'mergerequest' });
+ .reply(HTTP_STATUS_OK, { title: 'mergerequest' });
});
it('calls getProjectMergeRequestChanges service method', async () => {
@@ -276,7 +279,7 @@ describe('IDE store merge request actions', () => {
beforeEach(() => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
- .reply(200, [{ id: 789 }]);
+ .reply(HTTP_STATUS_OK, [{ id: 789 }]);
jest.spyOn(service, 'getProjectMergeRequestVersions');
});
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index 6e8a03b47ad..47b6ebb3376 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -8,6 +8,7 @@ import { createStore } from '~/ide/stores';
import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { file, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
@@ -52,7 +53,7 @@ describe('Multi-file store tree actions', () => {
mock
.onGet(/(.*)/)
- .replyOnce(200, [
+ .replyOnce(HTTP_STATUS_OK, [
'file.txt',
'folder/fileinfolder.js',
'folder/subfolder/fileinsubfolder.js',
@@ -98,7 +99,7 @@ describe('Multi-file store tree actions', () => {
findBranch: () => store.state.projects['abc/def'].branches['main-testing'],
};
- mock.onGet(/(.*)/).replyOnce(500);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await expect(
getFiles(
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index fd2c3d18813..1c90c0f943a 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -23,6 +23,7 @@ import {
} from '~/ide/stores/actions';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
@@ -917,7 +918,7 @@ describe('Multi-file store actions', () => {
});
it('passes the error further unchanged without dispatching any action when response is 404', async () => {
- mock.onGet(/(.*)/).replyOnce(404);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_NOT_FOUND);
await expect(getBranchData(...callParams)).rejects.toEqual(
new Error('Request failed with status code 404'),
@@ -927,7 +928,7 @@ describe('Multi-file store actions', () => {
});
it('does not pass the error further and flashes an alert if error is not 404', async () => {
- mock.onGet(/(.*)/).replyOnce(418);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT);
await expect(getBranchData(...callParams)).rejects.toEqual(
new Error('Branch not loaded - <strong>abc/def/main-testing</strong>'),
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 24661e21cd0..d4166a3bd6d 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -294,18 +294,6 @@ describe('IDE store getters', () => {
});
});
- describe('packageJson', () => {
- it('returns package.json entry', () => {
- localState.entries['package.json'] = {
- name: 'package.json',
- };
-
- expect(getters.packageJson(localState)).toEqual({
- name: 'package.json',
- });
- });
- });
-
describe('canPushToBranch', () => {
it.each`
currentBranch | canPushCode | expectedValue
diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js
index 306330e3ba2..c1c47ef7e9a 100644
--- a/spec/frontend/ide/stores/modules/branches/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/branches/actions_spec.js
@@ -10,6 +10,7 @@ import {
import * as types from '~/ide/stores/modules/branches/mutation_types';
import state from '~/ide/stores/modules/branches/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { branches, projectData } from '../../../mock_data';
describe('IDE branches actions', () => {
@@ -94,7 +95,9 @@ describe('IDE branches actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(200, branches);
+ mock
+ .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, branches);
});
it('calls API with params', () => {
@@ -124,7 +127,9 @@ describe('IDE branches actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(500);
+ mock
+ .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
deleted file mode 100644
index c2b9de192d9..00000000000
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import testAction from 'helpers/vuex_action_helper';
-import { PING_USAGE_PREVIEW_KEY } from '~/ide/constants';
-import * as actions from '~/ide/stores/modules/clientside/actions';
-import axios from '~/lib/utils/axios_utils';
-
-const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
-const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/${PING_USAGE_PREVIEW_KEY}`;
-
-describe('IDE store module clientside actions', () => {
- let rootGetters;
- let mock;
-
- beforeEach(() => {
- rootGetters = {
- currentProject: {
- web_url: TEST_PROJECT_URL,
- },
- };
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('pingUsage', () => {
- it('posts to usage endpoint', async () => {
- const usageSpy = jest.fn(() => [200]);
-
- mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
-
- await testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], []);
- expect(usageSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 8601e13f7ca..4068a9d0919 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -14,6 +14,7 @@ import {
COMMIT_TO_NEW_BRANCH,
} from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -48,7 +49,7 @@ describe('IDE commit module actions', () => {
mock
.onGet('/api/v1/projects/abcproject/repository/branches/main')
- .reply(200, { commit: COMMIT_RESPONSE });
+ .reply(HTTP_STATUS_OK, { commit: COMMIT_RESPONSE });
});
afterEach(() => {
diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
index 1080a30d2d8..a5ce507bd3c 100644
--- a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/ide/stores/modules/file_templates/actions';
import * as types from '~/ide/stores/modules/file_templates/mutation_types';
import createState from '~/ide/stores/modules/file_templates/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('IDE file templates actions', () => {
let state;
@@ -74,7 +75,7 @@ describe('IDE file templates actions', () => {
const page = pages[pageNum - 1];
const hasNextPage = pageNum < pages.length;
- return [200, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
+ return [HTTP_STATUS_OK, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
});
});
@@ -108,7 +109,7 @@ describe('IDE file templates actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(500);
+ mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches actions', () => {
@@ -199,10 +200,10 @@ describe('IDE file templates actions', () => {
beforeEach(() => {
mock
.onGet(/api\/(.*)\/templates\/licenses\/mit/)
- .replyOnce(200, { content: 'MIT content' });
+ .replyOnce(HTTP_STATUS_OK, { content: 'MIT content' });
mock
.onGet(/api\/(.*)\/templates\/licenses\/testing/)
- .replyOnce(200, { content: 'testing content' });
+ .replyOnce(HTTP_STATUS_OK, { content: 'testing content' });
});
it('dispatches setFileTemplate if template already has content', () => {
@@ -248,7 +249,9 @@ describe('IDE file templates actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(500);
+ mock
+ .onGet(/api\/(.*)\/templates\/licenses\/mit/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
index 344fe3a41c3..56901383f7b 100644
--- a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
@@ -10,6 +10,7 @@ import {
import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
import state from '~/ide/stores/modules/merge_requests/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { mergeRequests } from '../../../mock_data';
describe('IDE merge requests actions', () => {
@@ -80,7 +81,7 @@ describe('IDE merge requests actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests);
+ mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(HTTP_STATUS_OK, mergeRequests);
});
it('calls API with params', () => {
@@ -132,7 +133,9 @@ describe('IDE merge requests actions', () => {
describe('success without type', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests);
+ mock
+ .onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/)
+ .replyOnce(HTTP_STATUS_OK, mergeRequests);
});
it('calls API with project', () => {
@@ -169,7 +172,7 @@ describe('IDE merge requests actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index b76b673c3a2..f49ff75ba7e 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -25,6 +25,11 @@ import {
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
import state from '~/ide/stores/modules/pipelines/state';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { pipelines, jobs } from '../../../mock_data';
@@ -60,7 +65,7 @@ describe('IDE pipelines actions', () => {
it('commits error', () => {
return testAction(
receiveLatestPipelineError,
- { status: 404 },
+ { status: HTTP_STATUS_NOT_FOUND },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
[{ type: 'stopPipelinePolling' }],
@@ -70,7 +75,7 @@ describe('IDE pipelines actions', () => {
it('dispatches setErrorMessage is not 404', () => {
return testAction(
receiveLatestPipelineError,
- { status: 500 },
+ { status: HTTP_STATUS_INTERNAL_SERVER_ERROR },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
[
@@ -118,7 +123,7 @@ describe('IDE pipelines actions', () => {
beforeEach(() => {
mock
.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
- .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
+ .reply(HTTP_STATUS_OK, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
});
it('dispatches request', async () => {
@@ -151,7 +156,9 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
+ mock
+ .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', async () => {
@@ -238,7 +245,7 @@ describe('IDE pipelines actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
+ mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_OK, jobs);
});
it('dispatches request', () => {
@@ -257,7 +264,7 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(stage.dropdownPath).replyOnce(500);
+ mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -367,7 +374,7 @@ describe('IDE pipelines actions', () => {
describe('success', () => {
beforeEach(() => {
jest.spyOn(axios, 'get');
- mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' });
+ mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(HTTP_STATUS_OK, { html: 'html' });
});
it('dispatches request', () => {
@@ -397,7 +404,9 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(500);
+ mock
+ .onGet(`${TEST_HOST}/project/builds/trace`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
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 09be1e333b3..8d8afda7014 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -11,8 +11,11 @@ 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 {
+ HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
@@ -129,7 +132,7 @@ describe('IDE store terminal check actions', () => {
describe('fetchConfigCheck', () => {
it('dispatches request and receive', () => {
- mock.onPost(/.*\/ide_terminals\/check_config/).reply(200, {});
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_OK, {});
return testAction(
actions.fetchConfigCheck,
@@ -144,7 +147,7 @@ describe('IDE store terminal check actions', () => {
});
it('when error, dispatches request and receive', () => {
- mock.onPost(/.*\/ide_terminals\/check_config/).reply(400, {});
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_BAD_REQUEST, {});
return testAction(
actions.fetchConfigCheck,
@@ -252,7 +255,9 @@ describe('IDE store terminal check actions', () => {
describe('fetchRunnersCheck', () => {
it('dispatches request and receive', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_OK, []);
return testAction(
actions.fetchRunnersCheck,
@@ -264,7 +269,9 @@ describe('IDE store terminal check actions', () => {
});
it('does not dispatch request when background is true', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_OK, []);
return testAction(
actions.fetchRunnersCheck,
@@ -276,7 +283,9 @@ describe('IDE store terminal check actions', () => {
});
it('dispatches request and receive, when error', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(500, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []);
return testAction(
actions.fetchRunnersCheck,
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 9fd5f1a38d7..0287e5269ee 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,12 @@ 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 { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -111,7 +116,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on success', () => {
- mock.onPost(/.*\/ide_terminals/).reply(200, TEST_SESSION);
+ mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.startSession,
@@ -126,7 +131,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on error', () => {
- mock.onPost(/.*\/ide_terminals/).reply(400);
+ mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.startSession,
@@ -175,7 +180,7 @@ describe('IDE store terminal session controls actions', () => {
describe('stopSession', () => {
it('dispatches request and receive on success', () => {
- mock.onPost(TEST_SESSION.cancel_path).reply(200, {});
+ mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_OK, {});
const state = {
session: { cancelPath: TEST_SESSION.cancel_path },
@@ -191,7 +196,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on error', () => {
- mock.onPost(TEST_SESSION.cancel_path).reply(400);
+ mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_BAD_REQUEST);
const state = {
session: { cancelPath: TEST_SESSION.cancel_path },
@@ -254,7 +259,7 @@ describe('IDE store terminal session controls actions', () => {
it('dispatches request and receive on success', () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
- .reply(200, TEST_SESSION);
+ .reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.restartSession,
@@ -271,7 +276,7 @@ describe('IDE store terminal session controls actions', () => {
it('dispatches request and receive on error', () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
- .reply(400);
+ .reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.restartSession,
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index fe2328f25c2..9616733f052 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -6,6 +6,7 @@ import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/termin
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 { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -145,7 +146,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches success on success', () => {
- mock.onGet(state.session.showPath).reply(200, TEST_SESSION);
+ mock.onGet(state.session.showPath).reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.fetchSessionStatus,
@@ -157,7 +158,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches error on error', () => {
- mock.onGet(state.session.showPath).reply(400);
+ mock.onGet(state.session.showPath).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.fetchSessionStatus,
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index da7fb4e060d..163a60bae36 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -39,7 +39,7 @@ describe('import actions cell', () => {
describe('when group is finished', () => {
beforeEach(() => {
- createComponent({ isAvailableForImport: true, isFinished: true });
+ createComponent({ isAvailableForImport: false, isFinished: true });
});
it('renders re-import button', () => {
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 bd79e20e698..c7bda5a60ec 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
@@ -1,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
-import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
@@ -38,7 +38,9 @@ describe('import table', () => {
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
+ generateFakeEntry({ id: 3, status: STATUSES.NONE }),
];
+
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const FAKE_VERSION_VALIDATION = {
features: {
@@ -59,12 +61,14 @@ describe('import table', () => {
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
const findNewPathCol = () => wrapper.find('[data-test-id="new-path-col"]');
+ const findUnavailableFeaturesWarning = () =>
+ wrapper.find('[data-testid="unavailable-features-alert"]');
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
- const selectRow = (idx) =>
- wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
+ const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx);
+ const selectRow = (idx) => findRowCheckbox(idx).setChecked(true);
const createComponent = ({
bulkImportSourceGroups,
@@ -113,7 +117,7 @@ describe('import table', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- axiosMock.onGet(/.*\/exists$/, () => []).reply(200);
+ axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false });
});
afterEach(() => {
@@ -268,8 +272,6 @@ describe('import table', () => {
},
});
- axiosMock.onPost('/import/bulk_imports.json').reply(HTTP_STATUS_BAD_REQUEST);
-
await waitForPromises();
await findImportButtons()[0].trigger('click');
await waitForPromises();
@@ -281,6 +283,28 @@ describe('import table', () => {
);
});
+ it('displays inline error if importing group reports rate limit', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [FAKE_GROUP],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ importGroups: () => {
+ const error = new Error();
+ error.response = { status: HTTP_STATUS_TOO_MANY_REQUESTS };
+ throw error;
+ },
+ });
+
+ await waitForPromises();
+ await findImportButtons()[0].trigger('click');
+ await waitForPromises();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(wrapper.find('tbody tr').text()).toContain(i18n.ERROR_TOO_MANY_REQUESTS);
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
@@ -587,6 +611,40 @@ describe('import table', () => {
expect(tooltip.value).toBe('Path of the new group.');
});
+ describe('re-import', () => {
+ it('renders finished row as disabled by default', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeDefined();
+ });
+
+ it('enables row after clicking re-import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ const reimportButton = wrapper
+ .findAll('tbody td button')
+ .wrappers.find((w) => w.text().includes('Re-import'));
+
+ await reimportButton.trigger('click');
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeUndefined();
+ });
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
@@ -598,8 +656,8 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
- expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
+ expect(findUnavailableFeaturesWarning().exists()).toBe(true);
+ expect(findUnavailableFeaturesWarning().text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
@@ -617,7 +675,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findUnavailableFeaturesWarning().exists()).toBe(false);
});
});
diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 13d2a95ca14..4a1b85d24e3 100644
--- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -4,6 +4,7 @@ import { createAlert } from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
jest.mock('visibilityjs');
@@ -21,7 +22,7 @@ describe('Bulk import status poller', () => {
beforeEach(() => {
mockAdapter = new MockAdapter(axios);
- mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
+ mockAdapter.onGet(FAKE_POLL_PATH).reply(HTTP_STATUS_OK, {});
updateImportStatus = jest.fn();
poller = new StatusPoller({ updateImportStatus, pollPath: FAKE_POLL_PATH });
});
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 d686036781f..e613b9756af 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
@@ -1,4 +1,4 @@
-import { GlBadge, GlButton, GlDropdown } from '@gitlab/ui';
+import { GlBadge, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -31,12 +31,16 @@ describe('ProviderRepoTableRow', () => {
return store;
}
- const findImportButton = () => {
- const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === 'Import');
+ const findButton = (text) => {
+ const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === text);
return buttons.length ? buttons.at(0) : buttons;
};
+ const findImportButton = () => findButton('Import');
+ const findReimportButton = () => findButton('Re-import');
+ const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+
const findCancelButton = () => {
const buttons = wrapper
.findAllComponents(GlButton)
@@ -117,6 +121,10 @@ describe('ProviderRepoTableRow', () => {
optionalStages: OPTIONAL_STAGES,
});
});
+
+ it('does not render re-import button', () => {
+ expect(findReimportButton().exists()).toBe(false);
+ });
});
describe('when rendering importing project', () => {
@@ -200,19 +208,68 @@ describe('ProviderRepoTableRow', () => {
);
});
- it('does not renders a namespace select', () => {
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
+ it('does not render a namespace select', () => {
+ expect(findGroupDropdown().exists()).toBe(false);
});
it('does not render import button', () => {
expect(findImportButton().exists()).toBe(false);
});
+ it('renders re-import button', () => {
+ expect(findReimportButton().exists()).toBe(true);
+ });
+
+ it('renders namespace select after clicking re-import', async () => {
+ findReimportButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findGroupDropdown().exists()).toBe(true);
+ });
+
+ it('imports repo when clicking re-import button', async () => {
+ findReimportButton().vm.$emit('click');
+
+ await nextTick();
+
+ findReimportButton().vm.$emit('click');
+
+ expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ optionalStages: {},
+ });
+ });
+
it('passes stats to import status component', () => {
expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
});
});
+ describe('when rendering failed project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.FAILED,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('render import button', () => {
+ expect(findImportButton().exists()).toBe(true);
+ });
+ });
+
describe('when rendering incompatible project', () => {
const repo = {
importSource: {
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 4b34c21daa3..990587d4af7 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -21,6 +21,11 @@ import {
import state from '~/import_entities/import_projects/store/state';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_TOO_MANY_REQUESTS,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -93,7 +98,7 @@ describe('import_projects store actions', () => {
describe('with a successful request', () => {
it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -117,7 +122,7 @@ describe('import_projects store actions', () => {
});
it('commits SET_PAGE_CURSORS instead of SET_PAGE', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -141,7 +146,7 @@ describe('import_projects store actions', () => {
});
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
fetchRepos,
@@ -157,7 +162,7 @@ describe('import_projects store actions', () => {
let requestedUrl;
mock.onGet().reply((config) => {
requestedUrl = config.url;
- return [200, payload];
+ return [HTTP_STATUS_OK, payload];
});
const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
@@ -182,7 +187,7 @@ describe('import_projects store actions', () => {
let requestedUrl;
mock.onGet().reply((config) => {
requestedUrl = config.url;
- return [200, payload];
+ return [HTTP_STATUS_OK, payload];
});
const localStateWithPage = { ...localState, pageInfo: { endCursor: 'endTest' } };
@@ -201,7 +206,7 @@ describe('import_projects store actions', () => {
});
it('correctly keeps current page on an unsuccessful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const CURRENT_PAGE = 5;
return testAction(
@@ -215,7 +220,7 @@ describe('import_projects store actions', () => {
describe('when rate limited', () => {
it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(429);
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_TOO_MANY_REQUESTS);
await testAction(
fetchRepos,
@@ -233,7 +238,7 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
it('fetches repos with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -264,7 +269,7 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
const importedProject = { name: 'imported/project' };
- mock.onPost(MOCK_ENDPOINT).reply(200, importedProject);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, importedProject);
return testAction(
fetchImport,
@@ -288,7 +293,7 @@ describe('import_projects store actions', () => {
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(500);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(
fetchImport,
@@ -311,7 +316,9 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and 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 });
+ mock
+ .onPost(MOCK_ENDPOINT)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { errors: ERROR_MESSAGE });
await testAction(
fetchImport,
@@ -349,7 +356,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, updatedProjects);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, updatedProjects);
await testAction(
fetchJobs,
@@ -371,7 +378,9 @@ describe('import_projects store actions', () => {
});
it('fetches realtime changes with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json?filter=filter`)
+ .reply(HTTP_STATUS_OK, updatedProjects);
return testAction(
fetchJobs,
@@ -433,7 +442,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits CANCEL_IMPORT_SUCCESS on success', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(200);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_OK);
await testAction(
cancelImport,
@@ -450,7 +459,7 @@ describe('import_projects store actions', () => {
});
it('shows generic error message on an unsuccessful request', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(500);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
@@ -461,7 +470,9 @@ describe('import_projects store actions', () => {
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 });
+ mock
+ .onPost(MOCK_ENDPOINT)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { errors: ERROR_MESSAGE });
await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
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 7884e9b4307..514a168553a 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -8,14 +8,14 @@ describe('import_projects store mutations', () => {
const SOURCE_PROJECT = {
id: 1,
- full_name: 'full/name',
- sanitized_name: 'name',
- provider_link: 'https://demo.link/full/name',
+ fullName: 'full/name',
+ sanitizedName: 'name',
+ providerLink: 'https://demo.link/full/name',
};
const IMPORTED_PROJECT = {
name: 'demo',
importSource: 'something',
- providerLink: 'custom-link',
+ providerLink: 'https://demo.link/full/name',
importStatus: 'status',
fullName: 'fullName',
};
@@ -64,21 +64,15 @@ describe('import_projects store mutations', () => {
describe('for imported projects', () => {
const response = {
importedProjects: [IMPORTED_PROJECT],
- providerRepos: [],
+ providerRepos: [SOURCE_PROJECT],
};
- it('recreates importSource from response', () => {
+ it('adds importedProject to relevant provider repo', () => {
state = getInitialState();
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining({
- fullName: IMPORTED_PROJECT.importSource,
- sanitizedName: IMPORTED_PROJECT.name,
- providerLink: IMPORTED_PROJECT.providerLink,
- }),
- );
+ expect(state.repositories[0].importedProject).toStrictEqual(IMPORTED_PROJECT);
});
it('passes project to importProject', () => {
@@ -216,13 +210,13 @@ describe('import_projects store mutations', () => {
describe(`${types.RECEIVE_IMPORT_ERROR}`, () => {
beforeEach(() => {
const REPO_ID = 1;
- state = { repositories: [{ importSource: { id: REPO_ID } }] };
+ state = { repositories: [{ importSource: { id: REPO_ID }, importedProject: {} }] };
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
});
- it(`removes importedProject entry`, () => {
- expect(state.repositories[0].importedProject).toBeNull();
+ it('sets status to failed', () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.FAILED);
});
});
diff --git a/spec/frontend/import_entities/import_projects/utils_spec.js b/spec/frontend/import_entities/import_projects/utils_spec.js
index d705f0acbfe..42cdf0f5a19 100644
--- a/spec/frontend/import_entities/import_projects/utils_spec.js
+++ b/spec/frontend/import_entities/import_projects/utils_spec.js
@@ -19,7 +19,7 @@ describe('import_projects utils', () => {
it.each`
status | result
${STATUSES.FINISHED} | ${false}
- ${STATUSES.FAILED} | ${false}
+ ${STATUSES.FAILED} | ${true}
${STATUSES.SCHEDULED} | ${false}
${STATUSES.STARTED} | ${false}
${STATUSES.NONE} | ${true}
diff --git a/spec/frontend/integrations/index/components/integrations_table_spec.js b/spec/frontend/integrations/index/components/integrations_table_spec.js
index bfe0a5987b4..976c7b74890 100644
--- a/spec/frontend/integrations/index/components/integrations_table_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_table_spec.js
@@ -1,5 +1,6 @@
-import { GlTable, GlIcon } from '@gitlab/ui';
+import { GlTable, GlIcon, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { INTEGRATION_TYPE_SLACK } from '~/integrations/constants';
import IntegrationsTable from '~/integrations/index/components/integrations_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -10,12 +11,17 @@ describe('IntegrationsTable', () => {
const findTable = () => wrapper.findComponent(GlTable);
- const createComponent = (propsData = {}) => {
+ const createComponent = (propsData = {}, flagIsOn = false) => {
wrapper = mount(IntegrationsTable, {
propsData: {
integrations: mockActiveIntegrations,
...propsData,
},
+ provide: {
+ glFeatures: {
+ integrationSlackAppNotifications: flagIsOn,
+ },
+ },
});
};
@@ -50,4 +56,51 @@ describe('IntegrationsTable', () => {
expect(findTable().findComponent(GlIcon).exists()).toBe(shouldRenderActiveIcon);
});
});
+
+ describe('integrations filtering', () => {
+ const slackActive = {
+ ...mockActiveIntegrations[0],
+ name: INTEGRATION_TYPE_SLACK,
+ title: 'Slack',
+ };
+ const slackInactive = {
+ ...mockInactiveIntegrations[0],
+ name: INTEGRATION_TYPE_SLACK,
+ title: 'Slack',
+ };
+
+ describe.each`
+ desc | flagIsOn | integrations | expectedIntegrations
+ ${'only active'} | ${false} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
+ ${'only active'} | ${true} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
+ ${'only inactive'} | ${true} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
+ ${'only inactive'} | ${false} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
+ ${'active and inactive'} | ${true} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'active and inactive'} | ${false} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack active with active'} | ${false} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
+ ${'Slack active with active'} | ${true} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
+ ${'Slack active with inactive'} | ${false} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
+ ${'Slack active with inactive'} | ${true} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active'} | ${false} | ${[slackInactive, ...mockActiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations]}
+ ${'Slack inactive with active'} | ${true} | ${[slackInactive, ...mockActiveIntegrations]} | ${mockActiveIntegrations}
+ ${'Slack inactive with inactive'} | ${false} | ${[slackInactive, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockInactiveIntegrations]}
+ ${'Slack inactive with inactive'} | ${true} | ${[slackInactive, ...mockInactiveIntegrations]} | ${mockInactiveIntegrations}
+ ${'Slack active with active and inactive'} | ${true} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack active with active and inactive'} | ${false} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active and inactive'} | ${true} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active and inactive'} | ${false} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ `('when $desc and flag "$flagIsOn"', ({ flagIsOn, integrations, expectedIntegrations }) => {
+ beforeEach(() => {
+ createComponent({ integrations }, flagIsOn);
+ });
+
+ it('renders correctly', () => {
+ const links = wrapper.findAllComponents(GlLink);
+ expect(links).toHaveLength(expectedIntegrations.length);
+ expectedIntegrations.forEach((integration, index) => {
+ expect(links.at(index).text()).toBe(integration.title);
+ });
+ });
+ });
+ });
});
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 b6b34e1063b..9687d528321 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -2,6 +2,7 @@ import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gi
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -20,6 +21,8 @@ import {
LEARN_GITLAB,
EXPANDED_ERRORS,
EMPTY_INVITES_ALERT_TEXT,
+ ON_CELEBRATION_TRACK_LABEL,
+ INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -58,6 +61,13 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('InviteMembersModal', () => {
let wrapper;
let mock;
+ let trackingSpy;
+
+ const expectTracking = (
+ action,
+ label = undefined,
+ category = INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ ) => expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
@@ -66,6 +76,7 @@ describe('InviteMembersModal', () => {
},
propsData: {
usersLimitDataset: {},
+ activeTrialDataset: {},
fullPath: 'project',
...propsData,
...props,
@@ -83,12 +94,20 @@ describe('InviteMembersModal', () => {
});
};
- const createInviteMembersToProjectWrapper = (usersLimitDataset = {}, stubs = {}) => {
- createComponent({ usersLimitDataset, isProject: true }, stubs);
+ const createInviteMembersToProjectWrapper = (
+ usersLimitDataset = {},
+ activeTrialDataset = {},
+ stubs = {},
+ ) => {
+ createComponent({ usersLimitDataset, activeTrialDataset, isProject: true }, stubs);
};
- const createInviteMembersToGroupWrapper = (usersLimitDataset = {}, stubs = {}) => {
- createComponent({ usersLimitDataset, isProject: false }, stubs);
+ const createInviteMembersToGroupWrapper = (
+ usersLimitDataset = {},
+ activeTrialDataset = {},
+ stubs = {},
+ ) => {
+ createComponent({ usersLimitDataset, activeTrialDataset, isProject: false }, stubs);
};
beforeEach(() => {
@@ -129,7 +148,7 @@ describe('InviteMembersModal', () => {
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji);
- const triggerOpenModal = async ({ mode = 'default', source }) => {
+ const triggerOpenModal = async ({ mode = 'default', source } = {}) => {
eventHub.$emit('openModal', { mode, source });
await nextTick();
};
@@ -291,7 +310,7 @@ describe('InviteMembersModal', () => {
});
});
- describe('displaying the correct introText and form group description', () => {
+ describe('rendering with tracking considerations', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
beforeEach(() => {
@@ -318,7 +337,7 @@ describe('InviteMembersModal', () => {
describe('when inviting members with celebration', () => {
beforeEach(async () => {
createInviteMembersToProjectWrapper();
- await triggerOpenModal({ mode: 'celebrate' });
+ await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
});
it('renders the modal with confetti', () => {
@@ -344,6 +363,26 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
+
+ describe('tracking', () => {
+ it('tracks actions', async () => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ const mockEvent = { preventDefault: jest.fn() };
+
+ await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
+
+ expectTracking('render', ON_CELEBRATION_TRACK_LABEL);
+
+ findModal().vm.$emit('cancel', mockEvent);
+ expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL);
+
+ findModal().vm.$emit('close');
+ expectTracking('click_x', ON_CELEBRATION_TRACK_LABEL);
+
+ unmockTracking();
+ });
+ });
});
});
@@ -361,6 +400,32 @@ describe('InviteMembersModal', () => {
});
});
});
+
+ describe('tracking', () => {
+ it.each`
+ desc | source | label
+ ${'unknown'} | ${{}} | ${'unknown'}
+ ${'known'} | ${{ source: '_invite_source_' }} | ${'_invite_source_'}
+ `('tracks actions with $desc source', async ({ source, label }) => {
+ createInviteMembersToProjectWrapper();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ const mockEvent = { preventDefault: jest.fn() };
+
+ await triggerOpenModal(source);
+
+ expectTracking('render', label);
+
+ findModal().vm.$emit('cancel', mockEvent);
+ expectTracking('click_cancel', label);
+
+ findModal().vm.$emit('close');
+ expectTracking('click_x', label);
+
+ unmockTracking();
+ });
+ });
});
describe('rendering the user limit notification', () => {
@@ -625,6 +690,7 @@ describe('InviteMembersModal', () => {
createComponent();
await triggerMembersTokenSelect([user3]);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
@@ -842,17 +908,23 @@ describe('InviteMembersModal', () => {
createComponent();
await triggerMembersTokenSelect([user1, user3]);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
describe('when triggered from regular mounting', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ await triggerOpenModal({ source: '_invite_source_' });
+
clickInviteButton();
});
- it('calls Api inviteGroupMembers with the correct params', () => {
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ it('calls Api inviteGroupMembers with the correct params and invite source', () => {
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
+ ...postData,
+ invite_source: '_invite_source_',
+ });
});
it('displays the successful toastMessage', () => {
@@ -866,17 +938,20 @@ describe('InviteMembersModal', () => {
it('does not call reloadOnInvitationSuccess', () => {
expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
});
+
+ it('tracks successful invite when source is known', () => {
+ expectTracking('invite_successful', '_invite_source_');
+
+ unmockTracking();
+ });
});
- it('calls Apis with the invite source passed through to openModal', async () => {
- await triggerOpenModal({ source: '_invite_source_' });
+ it('calls Apis without the invite source passed through to openModal', async () => {
+ await triggerOpenModal();
clickInviteButton();
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
- ...postData,
- invite_source: '_invite_source_',
- });
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
});
});
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 db2afbbd141..f34f9902514 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -70,6 +70,11 @@ describe('InviteModalBase', () => {
const findActionButton = () => wrapper.find('.js-modal-action-primary');
describe('rendering the modal', () => {
+ let trackingSpy;
+
+ const expectTracking = (action, label = undefined, category = undefined) =>
+ expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
+
beforeEach(() => {
createComponent();
});
@@ -151,14 +156,6 @@ describe('InviteModalBase', () => {
});
describe('when users limit is reached', () => {
- let trackingSpy;
-
- const expectTracking = (action, label) =>
- expect(trackingSpy).toHaveBeenCalledWith('default', action, {
- label,
- category: 'default',
- });
-
beforeEach(() => {
createComponent(
{ props: { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } } },
@@ -176,7 +173,7 @@ describe('InviteModalBase', () => {
const modal = wrapper.findComponent(GlModal);
modal.vm.$emit('shown');
- expectTracking('render', ON_SHOW_TRACK_LABEL);
+ expectTracking('render', ON_SHOW_TRACK_LABEL, 'default');
unmockTracking();
});
diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js
index acc062b5fff..6fbf95362fa 100644
--- a/spec/frontend/invite_members/components/project_select_spec.js
+++ b/spec/frontend/invite_members/components/project_select_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByType, GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import * as projectsApi from '~/api/projects_api';
@@ -9,7 +9,12 @@ describe('ProjectSelect', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(ProjectSelect, {});
+ wrapper = shallowMountExtended(ProjectSelect, {
+ stubs: {
+ GlCollapsibleListbox,
+ GlAvatarLabeled,
+ },
+ });
};
beforeEach(() => {
@@ -22,16 +27,24 @@ describe('ProjectSelect', () => {
wrapper.destroy();
});
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findAvatarLabeled = (index) => findDropdownItem(index).findComponent(GlAvatarLabeled);
- const findEmptyResultMessage = () => wrapper.findByTestId('empty-result-message');
- const findErrorMessage = () => wrapper.findByTestId('error-message');
-
- it('renders GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search projects',
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index);
+
+ it('renders GlCollapsibleListbox with default props', () => {
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props()).toMatchObject({
+ items: [],
+ loading: false,
+ multiple: false,
+ noResultsText: 'No matching results',
+ placement: 'left',
+ searchPlaceholder: 'Search projects',
+ searchable: true,
+ searching: false,
+ size: 'medium',
+ toggleText: 'Select a project',
+ totalItems: null,
+ variant: 'default',
});
});
@@ -48,7 +61,7 @@ describe('ProjectSelect', () => {
}),
);
- findSearchBoxByType().vm.$emit('input', project1.name);
+ findGlCollapsibleListbox().vm.$emit('search', project1.name);
});
it('calls the API', () => {
@@ -61,14 +74,12 @@ describe('ProjectSelect', () => {
});
it('displays loading icon while waiting for API call to resolve and then sets loading false', async () => {
- expect(findSearchBoxByType().props('isLoading')).toBe(true);
+ expect(findGlCollapsibleListbox().props('searching')).toBe(true);
resolveApiRequest({ data: allProjects });
await waitForPromises();
- expect(findSearchBoxByType().props('isLoading')).toBe(false);
- expect(findEmptyResultMessage().exists()).toBe(false);
- expect(findErrorMessage().exists()).toBe(false);
+ expect(findGlCollapsibleListbox().props('searching')).toBe(false);
});
it('displays a dropdown item and avatar for each project fetched', async () => {
@@ -76,11 +87,11 @@ describe('ProjectSelect', () => {
await waitForPromises();
allProjects.forEach((project, index) => {
- expect(findDropdownItem(index).attributes('name')).toBe(project.name_with_namespace);
expect(findAvatarLabeled(index).attributes()).toMatchObject({
src: project.avatar_url,
'entity-id': String(project.id),
'entity-name': project.name_with_namespace,
+ size: '32',
});
expect(findAvatarLabeled(index).props('label')).toBe(project.name_with_namespace);
});
@@ -90,16 +101,17 @@ describe('ProjectSelect', () => {
resolveApiRequest({ data: [] });
await waitForPromises();
- expect(findEmptyResultMessage().text()).toBe('No matching results');
+ expect(findGlCollapsibleListbox().text()).toBe('No matching results');
});
it('displays the error message when the fetch fails', async () => {
rejectApiRequest();
await waitForPromises();
- expect(findErrorMessage().text()).toBe(
- 'There was an error fetching the projects. Please try again.',
- );
+ // To be displayed in GlCollapsibleListbox once we implement
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2132
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/389974
+ expect(findGlCollapsibleListbox().text()).toBe('No matching results');
});
});
});
diff --git a/spec/frontend/invite_members/mock_data/api_response_data.js b/spec/frontend/invite_members/mock_data/api_response_data.js
index 9509422b603..4ab9026c531 100644
--- a/spec/frontend/invite_members/mock_data/api_response_data.js
+++ b/spec/frontend/invite_members/mock_data/api_response_data.js
@@ -6,7 +6,7 @@ export const project1 = {
};
export const project2 = {
id: 2,
- name: 'Project One',
+ name: 'Project Two',
name_with_namespace: 'Project Two',
avatar_url: 'test2',
};
diff --git a/spec/frontend/issuable/helpers.js b/spec/frontend/issuable/helpers.js
new file mode 100644
index 00000000000..632d69c2c88
--- /dev/null
+++ b/spec/frontend/issuable/helpers.js
@@ -0,0 +1,18 @@
+export function getSaveableFormChildren(form, exclude = ['input.js-toggle-draft']) {
+ const children = Array.from(form.children);
+ const saveable = children.filter((e) => {
+ const isFiltered = exclude.reduce(
+ ({ isFiltered: filtered, element }, selector) => {
+ return {
+ isFiltered: filtered || element.matches(selector),
+ element,
+ };
+ },
+ { isFiltered: false, element: e },
+ );
+
+ return !isFiltered.isFiltered;
+ });
+
+ return saveable;
+}
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 28ec0e22d8b..3e778e50fb8 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -4,6 +4,8 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { getSaveableFormChildren } from './helpers';
+
jest.mock('~/autosave');
const createIssuable = (form) => {
@@ -18,6 +20,7 @@ describe('IssuableForm', () => {
setHTMLFixture(`
<form>
<input name="[title]" />
+ <input type="checkbox" class="js-toggle-draft" />
<textarea name="[description]"></textarea>
</form>
`);
@@ -99,10 +102,11 @@ describe('IssuableForm', () => {
])('creates $id autosave when $id input exist', ({ id, input, selector }) => {
$form.append(input);
const $input = $form.find(selector);
- const totalAutosaveFormFields = $form.children().length;
createIssuable($form);
- expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields);
+ const children = getSaveableFormChildren($form[0]);
+
+ expect(Autosave).toHaveBeenCalledTimes(children.length);
expect(Autosave).toHaveBeenLastCalledWith(
$input.get(0),
['/', '', id],
@@ -153,12 +157,17 @@ describe('IssuableForm', () => {
});
});
- describe('wip', () => {
+ describe('draft', () => {
+ let titleField;
+ let toggleDraft;
+
beforeEach(() => {
instance = createIssuable($form);
+ titleField = document.querySelector('input[name="[title]"]');
+ toggleDraft = document.querySelector('input.js-toggle-draft');
});
- describe('removeWip', () => {
+ describe('removeDraft', () => {
it.each`
prefix
${'draFT: '}
@@ -169,25 +178,25 @@ describe('IssuableForm', () => {
${' (DrafT)'}
${'draft: [draft] (draft)'}
`('removes "$prefix" from the beginning of the title', ({ prefix }) => {
- instance.titleField.val(`${prefix}The Issuable's Title Value`);
+ titleField.value = `${prefix}The Issuable's Title Value`;
- instance.removeWip();
+ instance.removeDraft();
- expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ expect(titleField.value).toBe("The Issuable's Title Value");
});
});
- describe('addWip', () => {
+ describe('addDraft', () => {
it("properly adds the work in progress prefix to the Issuable's title", () => {
- instance.titleField.val("The Issuable's Title Value");
+ titleField.value = "The Issuable's Title Value";
- instance.addWip();
+ instance.addDraft();
- expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ expect(titleField.value).toBe("Draft: The Issuable's Title Value");
});
});
- describe('workInProgress', () => {
+ describe('isMarkedDraft', () => {
it.each`
title | expected
${'draFT: something is happening'} | ${true}
@@ -195,10 +204,45 @@ describe('IssuableForm', () => {
${'something is happening to drafts'} | ${false}
${'something is happening'} | ${false}
`('returns $expected with "$title"', ({ title, expected }) => {
- instance.titleField.val(title);
+ titleField.value = title;
- expect(instance.workInProgress()).toBe(expected);
+ expect(instance.isMarkedDraft()).toBe(expected);
});
});
+
+ describe('readDraftStatus', () => {
+ it.each`
+ title | checked
+ ${'Draft: my title'} | ${true}
+ ${'my title'} | ${false}
+ `(
+ 'sets the draft checkbox checked status to $checked when the title is $title',
+ ({ title, checked }) => {
+ titleField.value = title;
+
+ instance.readDraftStatus();
+
+ expect(toggleDraft.checked).toBe(checked);
+ },
+ );
+ });
+
+ describe('writeDraftStatus', () => {
+ it.each`
+ checked | title
+ ${true} | ${'Draft: my title'}
+ ${false} | ${'my title'}
+ `(
+ 'updates the title to $title when the draft checkbox checked status is $checked',
+ ({ checked, title }) => {
+ titleField.value = 'my title';
+ toggleDraft.checked = checked;
+
+ instance.writeDraftStatus();
+
+ expect(titleField.value).toBe(title);
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index 16d4459f597..72fcab63ba7 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,9 +1,10 @@
import { GlFormGroup } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import IssueToken from '~/related_issues/components/issue_token.vue';
-import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
+import { linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
id: 200,
@@ -125,7 +126,7 @@ describe('AddIssuableForm', () => {
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: '',
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
@@ -142,7 +143,7 @@ describe('AddIssuableForm', () => {
wrapper = shallowMount(AddIssuableForm, {
propsData: {
inputValue: '',
- issuableType: issuableTypesMap.EPIC,
+ issuableType: TYPE_EPIC,
pathIdSeparator,
pendingReferences: [],
},
@@ -156,9 +157,9 @@ describe('AddIssuableForm', () => {
describe('categorized issuables', () => {
it.each`
- issuableType | pathIdSeparator | contextHeader | contextFooter
- ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
- ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
+ issuableType | pathIdSeparator | contextHeader | contextFooter
+ ${TYPE_ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
+ ${TYPE_EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
@@ -184,7 +185,7 @@ describe('AddIssuableForm', () => {
propsData: {
inputValue: '',
showCategorizedIssues: true,
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 996b2406240..ff8d5073005 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -6,10 +6,10 @@ import {
issuable2,
issuable3,
} from 'jest/issuable/components/related_issuable_mock_data';
+import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import {
- issuableTypesMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
PathIdSeparator,
@@ -34,7 +34,7 @@ describe('RelatedIssuesBlock', () => {
wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
},
});
});
@@ -237,7 +237,7 @@ describe('RelatedIssuesBlock', () => {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
},
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index bedf8bcaf34..96c0b87e2cb 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -9,6 +9,11 @@ import {
} from 'jest/issuable/components/related_issuable_mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_CONFLICT,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { linkedIssueTypesMap } from '~/related_issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
@@ -24,7 +29,7 @@ describe('RelatedIssuesRoot', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(defaultProps.endpoint).reply(200, []);
+ mock.onGet(defaultProps.endpoint).reply(HTTP_STATUS_OK, []);
});
afterEach(() => {
@@ -59,7 +64,7 @@ describe('RelatedIssuesRoot', () => {
});
it('removes related issue on API success', async () => {
- mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+ mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_OK, { issues: [] });
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
await axios.waitForAll();
@@ -68,7 +73,7 @@ describe('RelatedIssuesRoot', () => {
});
it('does not remove related issue on API error', async () => {
- mock.onDelete(issuable1.referencePath).reply(422, {});
+ mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {});
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
await axios.waitForAll();
@@ -163,7 +168,7 @@ describe('RelatedIssuesRoot', () => {
});
it('submits pending issue as related issue', async () => {
- mock.onPost(defaultProps.endpoint).reply(200, {
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_OK, {
issuables: [issuable1],
result: {
message: 'something was successfully related',
@@ -182,7 +187,7 @@ describe('RelatedIssuesRoot', () => {
});
it('submits multiple pending issues as related issues', async () => {
- mock.onPost(defaultProps.endpoint).reply(200, {
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_OK, {
issuables: [issuable1, issuable2],
result: {
message: 'something was successfully related',
@@ -204,7 +209,7 @@ describe('RelatedIssuesRoot', () => {
it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
- mock.onPost(defaultProps.endpoint).reply(409, { message });
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_CONFLICT, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
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 841cea28ffc..77d5a0579a4 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -19,6 +19,7 @@ import {
setSortPreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql';
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';
@@ -27,13 +28,20 @@ import { scrollUp } from '~/lib/utils/scroll_utils';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_SEARCH_WITHIN,
+ TOKEN_TYPE_TYPE,
} 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';
+import {
+ emptyIssuesQueryResponse,
+ issuesCountsQueryResponse,
+ issuesQueryResponse,
+} from '../mock_data';
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
@@ -69,24 +77,24 @@ describe('IssuesDashboardApp component', () => {
defaultQueryResponse.data.issues.nodes[0].weight = 5;
}
- const findCalendarButton = () =>
- wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
+ const findCalendarButton = () => wrapper.findByRole('link', { name: i18n.calendarLabel });
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 findRssButton = () => wrapper.findByRole('link', { name: i18n.rssLabel });
const mountComponent = ({
provide = {},
issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
- sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ issuesCountsQueryHandler = jest.fn().mockResolvedValue(issuesCountsQueryResponse),
+ sortPreferenceMutationHandler = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
} = {}) => {
wrapper = mountExtended(IssuesDashboardApp, {
apolloProvider: createMockApollo([
[getIssuesQuery, issuesQueryHandler],
- [setSortPreferenceMutation, sortPreferenceMutationResponse],
+ [getIssuesCountsQuery, issuesCountsQueryHandler],
+ [setSortPreferenceMutation, sortPreferenceMutationHandler],
]),
provide: {
...defaultProvide,
@@ -112,7 +120,9 @@ describe('IssuesDashboardApp component', () => {
return waitForPromises();
});
- it('renders IssuableList component', () => {
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/391722
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
currentTab: IssuableStates.Opened,
hasNextPage: true,
@@ -123,13 +133,18 @@ describe('IssuesDashboardApp component', () => {
issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ searchInputPlaceholder: i18n.searchPlaceholder,
showPaginationControls: true,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
}),
+ tabCounts: {
+ opened: 1,
+ closed: 2,
+ all: 3,
+ },
tabs: IssuesDashboardApp.IssuableListTabs,
urlParams: {
sort: urlSortParams[CREATED_DESC],
@@ -192,9 +207,9 @@ describe('IssuesDashboardApp component', () => {
it('renders empty state', () => {
expect(findEmptyState().props()).toMatchObject({
- description: IssuesDashboardApp.i18n.emptyStateWithFilterDescription,
+ description: i18n.noSearchResultsDescription,
svgPath: defaultProvide.emptyStateWithFilterSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateWithFilterTitle,
+ title: i18n.noSearchResultsTitle,
});
});
});
@@ -217,7 +232,7 @@ describe('IssuesDashboardApp component', () => {
expect(findEmptyState().props()).toMatchObject({
description: null,
svgPath: defaultProvide.emptyStateWithoutFilterSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateWithoutFilterTitle,
+ title: i18n.noSearchNoFilterTitle,
});
});
});
@@ -286,20 +301,28 @@ describe('IssuesDashboardApp component', () => {
});
});
- describe('when there is an error fetching issues', () => {
- beforeEach(() => {
- setWindowLocation(locationSearch);
- mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
- jest.runOnlyPendingTimers();
- return waitForPromises();
- });
+ describe('errors', () => {
+ describe.each`
+ error | mountOption | message
+ ${'fetching issues'} | ${'issuesQueryHandler'} | ${i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'issuesCountsQueryHandler'} | ${i18n.errorFetchingCounts}
+ `('when there is an error $error', ({ mountOption, message }) => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent({ [mountOption]: 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('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(message);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
});
it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error()) });
+
findIssuableList().vm.$emit('dismiss-alert');
await nextTick();
@@ -337,9 +360,12 @@ describe('IssuesDashboardApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
+ { type: TOKEN_TYPE_TYPE },
]);
});
});
@@ -401,7 +427,7 @@ describe('IssuesDashboardApp component', () => {
describe('when user is signed in', () => {
it('calls mutation to save sort preference', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
- mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ mountComponent({ sortPreferenceMutationHandler: mutationMock });
findIssuableList().vm.$emit('sort', UPDATED_DESC);
@@ -412,7 +438,7 @@ describe('IssuesDashboardApp component', () => {
const mutationMock = jest
.fn()
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
- mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ mountComponent({ sortPreferenceMutationHandler: mutationMock });
findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
@@ -426,7 +452,7 @@ describe('IssuesDashboardApp component', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({
provide: { isSignedIn: false },
- sortPreferenceMutationResponse: mutationMock,
+ sortPreferenceMutationHandler: mutationMock,
});
findIssuableList().vm.$emit('sort', CREATED_DESC);
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
index feb4cb80bd8..e789360d1d5 100644
--- a/spec/frontend/issues/dashboard/mock_data.js
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -86,3 +86,17 @@ export const emptyIssuesQueryResponse = {
},
},
};
+
+export const issuesCountsQueryResponse = {
+ data: {
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 2,
+ },
+ allIssues: {
+ count: 3,
+ },
+ },
+};
diff --git a/spec/frontend/issues/dashboard/utils_spec.js b/spec/frontend/issues/dashboard/utils_spec.js
index 08d00eee3e3..6a1fe6e4d70 100644
--- a/spec/frontend/issues/dashboard/utils_spec.js
+++ b/spec/frontend/issues/dashboard/utils_spec.js
@@ -3,6 +3,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { AutocompleteCache } from '~/issues/dashboard/utils';
import { MAX_LIST_SIZE } from '~/issues/list/constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AutocompleteCache', () => {
let autocompleteCache;
@@ -42,7 +43,7 @@ describe('AutocompleteCache', () => {
let response;
beforeEach(async () => {
- axiosMock.onGet(url).replyOnce(200, data);
+ axiosMock.onGet(url).replyOnce(HTTP_STATUS_OK, data);
response = await autocompleteCache.fetch({ url, cacheName, searchProperty });
});
@@ -59,7 +60,7 @@ describe('AutocompleteCache', () => {
let response;
beforeEach(async () => {
- axiosMock.onGet(url).replyOnce(200, data);
+ axiosMock.onGet(url).replyOnce(HTTP_STATUS_OK, data);
jest.spyOn(fuzzaldrinPlus, 'filter');
// Populate cache
await autocompleteCache.fetch({ url, cacheName, searchProperty });
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 089ea8dbbad..f04e766a78c 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -4,6 +4,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Issue', () => {
let testContext;
@@ -11,7 +12,7 @@ describe('Issue', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/related_branches$/).reply(200, {});
+ mock.onGet(/(.*)\/related_branches$/).reply(HTTP_STATUS_OK, {});
testContext = {};
testContext.issue = new Issue();
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index 065139f10f4..0a2e4e7c671 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -2,7 +2,7 @@ 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 NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
import { i18n } from '~/issues/list/constants';
describe('EmptyStateWithoutAnyIssues component', () => {
@@ -32,7 +32,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
wrapper.findByRole('link', { name: i18n.noIssuesDescription });
const findJiraDocsLink = () =>
wrapper.findByRole('link', { name: 'Enable the Jira integration' });
- const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
const findNewIssueLink = () => wrapper.findByRole('link', { name: i18n.newIssueLabel });
const findNewProjectLink = () => wrapper.findByRole('link', { name: i18n.newProjectLabel });
@@ -47,7 +47,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
...provide,
},
stubs: {
- NewIssueDropdown: true,
+ NewResourceDropdown: true,
},
});
};
@@ -156,7 +156,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders', () => {
mountComponent({ props: { showNewIssueDropdown: true } });
- expect(findNewIssueDropdown().exists()).toBe(true);
+ expect(findNewResourceDropdown().exists()).toBe(true);
});
});
@@ -164,7 +164,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('does not render', () => {
mountComponent({ props: { showNewIssueDropdown: false } });
- expect(findNewIssueDropdown().exists()).toBe(false);
+ expect(findNewResourceDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index b0d3a63a8cf..ab4d023ee39 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -1,7 +1,7 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import { IssuableStatus } from '~/issues/constants';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
describe('CE IssueCardTimeInfo component', () => {
@@ -25,7 +25,7 @@ describe('CE IssueCardTimeInfo component', () => {
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
- state = IssuableStatus.Open,
+ state = STATUS_OPEN,
dueDate = issue.dueDate,
milestoneDueDate = issue.milestone.dueDate,
milestoneStartDate = issue.milestone.startDate,
@@ -102,7 +102,7 @@ describe('CE IssueCardTimeInfo component', () => {
it('does not render in red', () => {
wrapper = mountComponent({
dueDate: '2020-10-10',
- state: IssuableStatus.Closed,
+ state: STATUS_CLOSED,
});
expect(findDueDate().classes()).not.toContain('gl-text-red-500');
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 4c5d8ce3cd1..8281ce0ed1a 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -22,6 +22,7 @@ import {
urlParams,
} from 'jest/issues/list/mock_data';
import { createAlert, VARIANT_INFO } from '~/flash';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
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';
@@ -30,7 +31,7 @@ import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/con
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 NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
import {
CREATED_DESC,
RELATIVE_POSITION,
@@ -42,6 +43,7 @@ import eventHub from '~/issues/list/eventhub';
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 { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import {
@@ -51,7 +53,6 @@ import {
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
import {
- FILTERED_SEARCH_TERM,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -98,7 +99,6 @@ describe('CE IssuesListApp component', () => {
hasScopedLabelsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
- isAnonymousSearchDisabled: false,
isIssueRepositioningDisabled: false,
isProject: true,
isPublicVisibilityRestricted: false,
@@ -129,7 +129,12 @@ describe('CE IssuesListApp component', () => {
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
const findIssuableList = () => wrapper.findComponent(IssuableList);
- const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
+
+ const findLabelsToken = () =>
+ findIssuableList()
+ .props('searchTokens')
+ .find((token) => token.type === TOKEN_TYPE_LABEL);
const mountComponent = ({
provide = {},
@@ -179,7 +184,7 @@ describe('CE IssuesListApp component', () => {
return waitForPromises();
});
- it('renders', () => {
+ it('renders', async () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
@@ -314,13 +319,13 @@ describe('CE IssuesListApp component', () => {
it('does not render in a project context', () => {
wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount });
- expect(findNewIssueDropdown().exists()).toBe(false);
+ expect(findNewResourceDropdown().exists()).toBe(false);
});
it('renders in a group context', () => {
wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
- expect(findNewIssueDropdown().exists()).toBe(true);
+ expect(findNewResourceDropdown().exists()).toBe(true);
});
});
});
@@ -426,27 +431,6 @@ describe('CE IssuesListApp component', () => {
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
});
-
- describe('when anonymous searching is performed', () => {
- beforeEach(() => {
- setWindowLocation(locationSearch);
- wrapper = mountComponent({
- provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
- });
- });
-
- it('is set from url params and removes search terms', () => {
- const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
- expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
- });
-
- it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: IssuesListApp.i18n.anonymousSearchingMessage,
- variant: VARIANT_INFO,
- });
- });
- });
});
});
@@ -585,7 +569,7 @@ describe('CE IssuesListApp component', () => {
it('renders all tokens alphabetically', () => {
const preloadedUsers = [
- { ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
+ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
@@ -782,7 +766,9 @@ describe('CE IssuesListApp component', () => {
});
it('displays an error message', async () => {
- axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
+ axiosMock
+ .onPut(joinPaths(issueOne.webPath, 'reorder'))
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
await waitForPromises();
@@ -903,29 +889,6 @@ describe('CE IssuesListApp component', () => {
query: expect.objectContaining(urlParams),
});
});
-
- describe('when anonymous searching is performed', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
- });
- router.push = jest.fn();
-
- findIssuableList().vm.$emit('filter', filteredTokens);
- });
-
- it('removes search terms', () => {
- const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
- expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
- });
-
- it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: IssuesListApp.i18n.anonymousSearchingMessage,
- variant: VARIANT_INFO,
- });
- });
- });
});
describe('when "page-size-change" event is emitted by IssuableList', () => {
@@ -983,4 +946,30 @@ describe('CE IssuesListApp component', () => {
);
});
});
+
+ describe('when providing token for labels', () => {
+ it('passes function to fetchLatestLabels property if frontend caching is enabled', () => {
+ wrapper = mountComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: true,
+ },
+ },
+ });
+
+ expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
+ });
+
+ it('passes null to fetchLatestLabels property if frontend caching is disabled', () => {
+ wrapper = mountComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: false,
+ },
+ },
+ });
+
+ expect(findLabelsToken().fetchLatestLabels).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
deleted file mode 100644
index 2c8cf9caf5d..00000000000
--- a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { mount, 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 NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
-import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
-import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- emptySearchProjectsQueryResponse,
- project1,
- project3,
- searchProjectsQueryResponse,
-} from '../mock_data';
-
-describe('NewIssueDropdown component', () => {
- let wrapper;
-
- Vue.use(VueApollo);
-
- const mountComponent = ({
- search = '',
- queryResponse = searchProjectsQueryResponse,
- mountFn = shallowMount,
- } = {}) => {
- const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]];
- const apolloProvider = createMockApollo(requestHandlers);
-
- return mountFn(NewIssueDropdown, {
- apolloProvider,
- provide: {
- fullPath: 'mushroom-kingdom',
- },
- data() {
- return { search };
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findInput = () => wrapper.findComponent(GlSearchBoxByType);
- const showDropdown = async () => {
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await waitForPromises();
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a split dropdown', () => {
- wrapper = mountComponent();
-
- expect(findDropdown().props('split')).toBe(true);
- });
-
- it('renders a label for the dropdown toggle button', () => {
- wrapper = mountComponent();
-
- expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel);
- });
-
- it('focuses on input when dropdown is shown', async () => {
- wrapper = mountComponent({ mountFn: mount });
-
- const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
-
- await showDropdown();
-
- expect(inputSpy).toHaveBeenCalledTimes(1);
- });
-
- it('renders projects with issues enabled', async () => {
- wrapper = mountComponent({ mountFn: mount });
- await showDropdown();
-
- const listItems = wrapper.findAll('li');
-
- expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
- expect(listItems.at(1).text()).toBe(project3.nameWithNamespace);
- });
-
- it('renders `No matches found` when there are no matches', async () => {
- wrapper = mountComponent({
- search: 'no matches',
- queryResponse: emptySearchProjectsQueryResponse,
- mountFn: mount,
- });
-
- await showDropdown();
-
- expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound);
- });
-
- describe('when no project is selected', () => {
- beforeEach(() => {
- wrapper = mountComponent();
- });
-
- it('dropdown button is not a link', () => {
- expect(findDropdown().attributes('split-href')).toBeUndefined();
- });
-
- it('displays default text on the dropdown button', () => {
- expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText);
- });
- });
-
- describe('when a project is selected', () => {
- beforeEach(async () => {
- wrapper = mountComponent({ mountFn: mount });
- await waitForPromises();
- await showDropdown();
-
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
- await waitForPromises();
- });
-
- it('dropdown button is a link', () => {
- const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new');
-
- expect(findDropdown().attributes('split-href')).toBe(href);
- });
-
- it('displays project name on the dropdown button', () => {
- expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`);
- });
- });
-});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 70b1521ff70..1e8a81116f3 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -25,6 +25,7 @@ export const getIssuesQueryResponse = {
id: '1',
__typename: 'Project',
issues: {
+ __persist: true,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
@@ -34,6 +35,7 @@ export const getIssuesQueryResponse = {
},
nodes: [
{
+ __persist: true,
__typename: 'Issue',
id: 'gid://gitlab/Issue/123456',
iid: '789',
@@ -57,6 +59,7 @@ export const getIssuesQueryResponse = {
assignees: {
nodes: [
{
+ __persist: true,
__typename: 'UserCore',
id: 'gid://gitlab/User/234',
avatarUrl: 'avatar/url',
@@ -67,6 +70,7 @@ export const getIssuesQueryResponse = {
],
},
author: {
+ __persist: true,
__typename: 'UserCore',
id: 'gid://gitlab/User/456',
avatarUrl: 'avatar/url',
@@ -77,6 +81,7 @@ export const getIssuesQueryResponse = {
labels: {
nodes: [
{
+ __persist: true,
id: 'gid://gitlab/ProjectLabel/456',
color: '#333',
title: 'Label title',
@@ -343,49 +348,3 @@ export const urlParamsWithSpecialValues = {
weight: 'None',
health_status: 'None',
};
-
-export const project1 = {
- id: 'gid://gitlab/Group/26',
- issuesEnabled: true,
- name: 'Super Mario Project',
- nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
-};
-
-export const project2 = {
- id: 'gid://gitlab/Group/59',
- issuesEnabled: false,
- name: 'Mario Kart Project',
- nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
-};
-
-export const project3 = {
- id: 'gid://gitlab/Group/103',
- issuesEnabled: true,
- name: 'Mario Party Project',
- nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
-};
-
-export const searchProjectsQueryResponse = {
- data: {
- group: {
- id: '1',
- projects: {
- nodes: [project1, project2, project3],
- },
- },
- },
-};
-
-export const emptySearchProjectsQueryResponse = {
- data: {
- group: {
- id: '1',
- projects: {
- nodes: [],
- },
- },
- },
-};
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index 8413b8463c1..010c719bd84 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import mockData from 'test_fixtures/issues/related_merge_requests.json';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RelatedMergeRequests from '~/issues/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/issues/related_merge_requests/store/index';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
@@ -18,7 +19,7 @@ describe('RelatedMergeRequests', () => {
document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(mockData);
mock = new MockAdapter(axios);
- mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
+ mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(HTTP_STATUS_OK, mockData, { 'x-total': 2 });
wrapper = shallowMount(RelatedMergeRequests, {
store: createStore(),
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 d3ec6c3bc9d..7339372a8d1 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -2,6 +2,7 @@ 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 { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
@@ -72,7 +73,9 @@ describe('RelatedMergeRequest store actions', () => {
describe('for a successful request', () => {
it('should dispatch success action', () => {
const data = { a: 1 };
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
+ mock
+ .onGet(`${state.apiEndpoint}?per_page=100`)
+ .replyOnce(HTTP_STATUS_OK, data, { 'x-total': 2 });
return testAction(
actions.fetchMergeRequests,
@@ -86,7 +89,7 @@ describe('RelatedMergeRequest store actions', () => {
describe('for a failing request', () => {
it('should dispatch error action', async () => {
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.fetchMergeRequests,
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 6cf44e60092..9fa0ce6f93d 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -6,7 +6,14 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
+import {
+ IssuableStatusText,
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ STATUS_REOPENED,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+} from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
import EditedComponent from '~/issues/show/components/edited.vue';
@@ -17,6 +24,7 @@ import PinnedLinks from '~/issues/show/components/pinned_links.vue';
import { POLLING_DELAY } from '~/issues/show/constants';
import eventHub from '~/issues/show/event_hub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
@@ -94,7 +102,7 @@ describe('Issuable output', () => {
mock
.onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
.reply(() => {
- const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
+ const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
realtimeRequestCount += 1;
return res;
});
@@ -330,7 +338,9 @@ describe('Issuable output', () => {
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
- mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
@@ -339,7 +349,9 @@ describe('Issuable output', () => {
it('shows the form if template names as array request is successful', () => {
const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
- mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
@@ -473,11 +485,11 @@ describe('Issuable output', () => {
});
it.each`
- issuableType | issuableStatus | statusIcon
- ${IssuableType.Issue} | ${IssuableStatus.Open} | ${'issues'}
- ${IssuableType.Issue} | ${IssuableStatus.Closed} | ${'issue-closed'}
- ${IssuableType.Epic} | ${IssuableStatus.Open} | ${'epic'}
- ${IssuableType.Epic} | ${IssuableStatus.Closed} | ${'epic-closed'}
+ issuableType | issuableStatus | statusIcon
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
async ({ issuableType, issuableStatus, statusIcon }) => {
@@ -491,9 +503,9 @@ describe('Issuable output', () => {
it.each`
title | state
- ${'shows with Open when status is opened'} | ${IssuableStatus.Open}
- ${'shows with Closed when status is closed'} | ${IssuableStatus.Closed}
- ${'shows with Open when status is reopened'} | ${IssuableStatus.Reopened}
+ ${'shows with Open when status is opened'} | ${STATUS_OPEN}
+ ${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
+ ${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
`('$title', async ({ state }) => {
wrapper.setProps({ issuableStatus: state });
@@ -645,10 +657,10 @@ describe('Issuable output', () => {
});
});
- describe('listItemReorder event', () => {
+ describe('saveDescription event', () => {
it('makes request to update issue', async () => {
const description = 'I have been updated!';
- findDescription().vm.$emit('listItemReorder', description);
+ findDescription().vm.$emit('saveDescription', description);
await waitForPromises();
expect(mock.history.put[0].data).toContain(description);
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 889ff450825..3f4513e6bfa 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlTooltip, GlModal } from '@gitlab/ui';
-
+import { GlModal } from '@gitlab/ui';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
@@ -10,23 +10,26 @@ import { mockTracking } from 'helpers/tracking_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
import { createAlert } from '~/flash';
import Description from '~/issues/show/components/description.vue';
+import eventHub from '~/issues/show/event_hub';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
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 {
+ createWorkItemMutationErrorResponse,
+ createWorkItemMutationResponse,
+ getIssueDetailsResponse,
projectWorkItemTypesQueryResponse,
- createWorkItemFromTaskMutationResponse,
} from 'jest/work_items/mock_data';
import {
descriptionProps as initialProps,
+ descriptionHtmlWithList,
descriptionHtmlWithCheckboxes,
descriptionHtmlWithTask,
} from '../mock_data/mock_data';
@@ -39,6 +42,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
+const mockSpriteIcons = '/icons.svg';
const showModal = jest.fn();
const hideModal = jest.fn();
const showDetailsModal = jest.fn();
@@ -46,6 +50,7 @@ const $toast = {
show: jest.fn(),
};
+const issueDetailsResponse = getIssueDetailsResponse();
const workItemQueryResponse = {
data: {
workItem: null,
@@ -54,44 +59,45 @@ const workItemQueryResponse = {
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
-const createWorkItemFromTaskSuccessHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemFromTaskMutationResponse);
describe('Description component', () => {
let wrapper;
+ let originalGon;
Vue.use(VueApollo);
const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
- const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
- const findConvertToTaskButton = () => wrapper.find('.js-add-task');
+ const findListItems = () => findGfmContent().findAll('ul > li');
+ const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
const findTaskLink = () => wrapper.find('a.gfm-issue');
-
- const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({
props = {},
provide,
- createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
+ createWorkItemMutationHandler,
+ ...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
+ issueIid: 1,
...initialProps,
...props,
},
provide: {
fullPath: 'gitlab-org/gitlab-test',
+ hasIterationsFeature: true,
...provide,
},
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
- [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
+ [getIssueDetailsQuery, issueDetailsQueryHandler],
+ [createWorkItemMutation, createWorkItemMutationHandler],
]),
mocks: {
$toast,
@@ -109,10 +115,14 @@ describe('Description component', () => {
},
}),
},
+ ...options,
});
}
beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { sprite_icons: mockSpriteIcons };
+
setWindowLocation(TEST_HOST);
if (!document.querySelector('.issuable-meta')) {
@@ -125,11 +135,9 @@ describe('Description component', () => {
}
});
- afterEach(() => {
- wrapper.destroy();
- });
-
afterAll(() => {
+ window.gon = originalGon;
+
$('.issuable-meta .flash-container').remove();
});
@@ -271,7 +279,38 @@ describe('Description component', () => {
});
});
- describe('with work_items_create_from_markdown feature flag enabled', () => {
+ describe('with list', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithList,
+ },
+ attachTo: document.body,
+ });
+ await nextTick();
+ });
+
+ it('shows list items', () => {
+ expect(findListItems()).toHaveLength(3);
+ });
+
+ it('shows list items drag icons', () => {
+ const dragIcon = findListItems().at(0).find('.drag-icon');
+
+ expect(dragIcon.classes()).toEqual(
+ expect.arrayContaining(['s14', 'gl-icon', 'gl-cursor-grab', 'gl-opacity-0']),
+ );
+ expect(dragIcon.attributes()).toMatchObject({
+ 'aria-hidden': 'true',
+ role: 'img',
+ });
+ expect(dragIcon.find('use').attributes()).toEqual({
+ href: `${mockSpriteIcons}#grip`,
+ });
+ });
+ });
+
+ describe('with work_items_mvc feature flag enabled', () => {
describe('empty description', () => {
beforeEach(() => {
createComponent({
@@ -280,7 +319,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItemsCreateFromMarkdown: true,
+ workItemsMvc: true,
},
},
});
@@ -300,7 +339,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItemsCreateFromMarkdown: true,
+ workItemsMvc: true,
},
},
});
@@ -311,13 +350,6 @@ describe('Description component', () => {
expect(findTaskActionButtons()).toHaveLength(3);
});
- it('renders a list of tooltips corresponding to checkboxes in description HTML', () => {
- expect(findTooltips()).toHaveLength(3);
- expect(findTooltips().at(0).props('target')).toBe(
- findTaskActionButtons().at(0).attributes('id'),
- );
- });
-
it('does not show a modal by default', () => {
expect(findModal().exists()).toBe(false);
});
@@ -331,50 +363,123 @@ describe('Description component', () => {
});
});
- describe('creating work item from checklist item', () => {
- it('emits `updateDescription` after creating new work item', async () => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsCreateFromMarkdown: true,
- },
- },
- });
+ describe('task list item actions', () => {
+ describe('converting the task list item to a task', () => {
+ describe('when successful', () => {
+ let createWorkItemMutationHandler;
- const newDescription = `<p>New description</p>`;
+ beforeEach(async () => {
+ createWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationResponse);
+ const descriptionText = `Tasks
- await findConvertToTaskButton().trigger('click');
+1. [ ] item 1
+ 1. [ ] item 2
- await waitForPromises();
+ paragraph text
- expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
- });
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ createComponent({
+ props: { descriptionText },
+ provide: { glFeatures: { workItemsMvc: true } },
+ createWorkItemMutationHandler,
+ });
+ await waitForPromises();
- it('shows flash message when creating task fails', async () => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsCreateFromMarkdown: true,
- },
- },
- createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
+ eventHub.$emit('convert-task-list-item', '4:4-8:19');
+ await waitForPromises();
+ });
+
+ it('emits an event to update the description with the deleted task list item omitted', () => {
+ const newDescriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
+
+ it('calls a mutation to create a task', () => {
+ const {
+ confidential,
+ iteration,
+ milestone,
+ } = issueDetailsResponse.data.workspace.issuable;
+ expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ confidential,
+ description: '\nparagraph text\n',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ iterationWidget: {
+ iterationId: IS_EE ? iteration.id : null,
+ },
+ milestoneWidget: {
+ milestoneId: milestone.id,
+ },
+ projectPath: 'gitlab-org/gitlab-test',
+ title: 'item 2',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ },
+ });
+ });
+
+ it('shows a toast to confirm the creation of the task', () => {
+ expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
+ });
});
- await findConvertToTaskButton().trigger('click');
+ describe('when unsuccessful', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: { descriptionText: 'description' },
+ provide: { glFeatures: { workItemsMvc: true } },
+ createWorkItemMutationHandler: jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationErrorResponse),
+ });
+ await waitForPromises();
- await waitForPromises();
+ eventHub.$emit('convert-task-list-item', '1:1-1:11');
+ await waitForPromises();
+ });
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: 'Something went wrong when creating task. Please try again.',
- }),
- );
+ it('shows an alert with an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong when creating task. Please try again.',
+ error: new Error('an error'),
+ captureError: true,
+ });
+ });
+ });
+ });
+
+ describe('deleting the task list item', () => {
+ it('emits an event to update the description with the deleted task list item', () => {
+ const descriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ const newDescriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ createComponent({
+ props: { descriptionText },
+ provide: { glFeatures: { workItemsMvc: true } },
+ });
+
+ eventHub.$emit('delete-task-list-item', '4:4-5:19');
+
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
});
});
@@ -386,7 +491,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItemsCreateFromMarkdown: true },
+ glFeatures: { workItemsMvc: true },
},
});
return nextTick();
@@ -448,7 +553,7 @@ describe('Description component', () => {
createComponent({
props: { descriptionHtml: descriptionHtmlWithTask },
- provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
+ provide: { glFeatures: { workItemsMvc: true } },
});
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
@@ -464,7 +569,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItemsCreateFromMarkdown: true },
+ glFeatures: { workItemsMvc: true },
},
});
return nextTick();
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 3333ceffca9..27ac0e1baf3 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -1,5 +1,5 @@
-import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -32,17 +32,16 @@ describe('Issue type field component', () => {
},
};
- const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup);
- const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown);
- const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findTypeFromDropDownItemAt = (at) => findTypeFromDropDownItems().at(at);
- const findTypeFromDropDownItemIconAt = (at) =>
- findTypeFromDropDownItems().at(at).findComponent(GlIcon);
+ const findListBox = () => wrapper.findComponent(GlListbox);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]');
+ const findIssueItemAt = (at) => findAllIssueItems().at(at);
+ const findIssueItemAtIcon = (at) => findAllIssueItems().at(at).findComponent(GlIcon);
- const createComponent = ({ data } = {}, provide) => {
+ const createComponent = (mountFn = mount, { data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers);
- wrapper = shallowMount(IssueTypeField, {
+ wrapper = mountFn(IssueTypeField, {
apolloProvider: fakeApollo,
data() {
return {
@@ -59,7 +58,6 @@ describe('Issue type field component', () => {
beforeEach(() => {
mockIssueStateData = jest.fn();
- createComponent();
});
afterEach(() => {
@@ -71,48 +69,60 @@ describe('Issue type field component', () => {
${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
- expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
- expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
+ createComponent();
+
+ expect(findIssueItemAtIcon(at).props('name')).toBe(icon);
+ expect(findIssueItemAt(at).text()).toBe(text);
});
it('renders a form group with the correct label', () => {
- expect(findTypeFromGroup().attributes('label')).toBe(i18n.label);
+ createComponent(shallowMount);
+
+ expect(findFormGroup().attributes('label')).toBe(i18n.label);
});
it('renders a form select with the `issue_type` value', () => {
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ createComponent();
+
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
+ createComponent();
+
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
- findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
+ createComponent();
+
+ wrapper.vm.$emit('select', issuableTypes.incident);
await nextTick();
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
it('hides the incident type from the dropdown', async () => {
- createComponent({}, { canCreateIncident: false, issueType: 'issue' });
+ createComponent(mount, {}, { canCreateIncident: false, issueType: 'issue' });
+
await waitForPromises();
- expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
- expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ expect(findIssueItemAt(0).isVisible()).toBe(true);
+ expect(findIssueItemAt(1).isVisible()).toBe(false);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
- createComponent({}, { canCreateIncident: false, issueType: 'incident' });
+ createComponent(mount, {}, { canCreateIncident: false, issueType: 'incident' });
+
await waitForPromises();
- expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
- expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
+ expect(findIssueItemAt(0).isVisible()).toBe(true);
+ expect(findIssueItemAt(1).isVisible()).toBe(true);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.incident);
});
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index aaf228ae181..3d9dad3a721 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { IssuableStatus, IssueType } from '~/issues/constants';
+import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
@@ -40,7 +40,7 @@ describe('HeaderActions component', () => {
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath: '-/abuse_reports/add_category',
- reportedUserId: '1',
+ reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
};
@@ -81,7 +81,7 @@ describe('HeaderActions component', () => {
const mountComponent = ({
props = {},
- issueState = IssuableStatus.Open,
+ issueState = STATUS_OPEN,
blockedByIssues = [],
mutateResponse = {},
} = {}) => {
@@ -123,9 +123,9 @@ describe('HeaderActions component', () => {
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
- description | issueState | buttonText | newIssueState
- ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
- ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
+ description | issueState | buttonText | newIssueState
+ ${`when the ${issueType} is open`} | ${STATUS_OPEN} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
+ ${`when the ${issueType} is closed`} | ${STATUS_CLOSED} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@@ -411,9 +411,8 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
- it('renders', () => {
- expect(findAbuseCategorySelector().exists()).toBe(true);
- expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ it("doesn't render", async () => {
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
});
it('opens the drawer', async () => {
@@ -425,9 +424,10 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
+ await findDesktopDropdownItems().at(2).vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
- expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
});
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 81c3c30bf8a..9159b742106 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
@@ -22,7 +22,11 @@ describe('Edit Timeline events', () => {
const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm);
- const mockSaveData = { ...fakeEventData, ...mockInputData };
+ const mockSaveData = {
+ ...fakeEventData,
+ ...mockInputData,
+ timelineEventTags: ['Start time', 'End time'],
+ };
describe('editTimelineEvent', () => {
const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] };
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 6606bed1567..f6951864344 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -1,3 +1,16 @@
+export const mockTimelineEventTags = {
+ nodes: [
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'Start time',
+ },
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'End time',
+ },
+ ],
+};
+
export const mockEvents = [
{
action: 'comment',
@@ -32,18 +45,7 @@ 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',
- },
- ],
- },
+ timelineEventTags: mockTimelineEventTags,
__typename: 'TimelineEventType',
},
{
@@ -187,5 +189,12 @@ export const mockInputData = {
occurredAt: '2020-08-10T02:30:00.000Z',
};
-const { id, note, occurredAt } = mockEvents[0];
-export const fakeEventData = { id, note, occurredAt };
+const { id, note, occurredAt, timelineEventTags } = mockEvents[0];
+export const fakeEventData = { id, note, occurredAt, timelineEventTags };
+export const fakeEventSaveData = {
+ id,
+ note,
+ occurredAt,
+ timelineEventTagNames: timelineEventTags,
+ ...mockInputData,
+};
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 f06d968a4c5..e352f9708e4 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
@@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import { GlDatepicker, GlListbox } from '@gitlab/ui';
+import { GlDatepicker, GlCollapsibleListbox } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
@@ -62,7 +62,7 @@ describe('Timeline events form', () => {
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
- const findTagDropdown = () => wrapper.findComponent(GlListbox);
+ const findTagsListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findTextarea = () => wrapper.findByTestId('input-note');
const findTextareaValue = () => findTextarea().element.value;
const findCountNumeric = (count) => wrapper.findByText(count);
@@ -75,7 +75,7 @@ describe('Timeline events form', () => {
findMinuteInput().setValue(45);
};
const selectTags = async (tags) => {
- findTagDropdown().vm.$emit(
+ findTagsListbox().vm.$emit(
'select',
tags.map((x) => x.value),
);
@@ -125,31 +125,31 @@ describe('Timeline events form', () => {
);
});
- describe('event tag dropdown', () => {
+ describe('event tags listbox', () => {
it('should render option list from provided array', () => {
- expect(findTagDropdown().props('items')).toEqual(mockTags);
+ expect(findTagsListbox().props('items')).toEqual(mockTags);
});
it('should allow to choose multiple tags', async () => {
await selectTags(mockTags);
- expect(findTagDropdown().props('selected')).toEqual(mockTags.map((x) => x.value));
+ expect(findTagsListbox().props('selected')).toEqual(mockTags.map((x) => x.value));
});
it('should show default option, when none is chosen', () => {
- expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
});
it('should show the tag, when one is selected', async () => {
await selectOneTag();
- expect(findTagDropdown().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
+ expect(findTagsListbox().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
});
it('should show the number of selected tags, when more than one is selected', async () => {
await selectTags(mockTags);
- expect(findTagDropdown().props('toggleText')).toBe('2 tags');
+ expect(findTagsListbox().props('toggleText')).toBe(`${mockTags.length} tags`);
});
it('should be cleared when clear is triggered', async () => {
@@ -159,8 +159,8 @@ describe('Timeline events form', () => {
wrapper.vm.clear();
await nextTick();
- expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
- expect(findTagDropdown().props('selected')).toEqual([]);
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagsListbox().props('selected')).toEqual([]);
});
it('should populate incident note with tags if a note was empty', async () => {
@@ -190,6 +190,33 @@ describe('Timeline events form', () => {
expect(findTextareaValue()).toBe('hello');
});
});
+
+ describe('form button behaviour', () => {
+ it('should enable the save buttons when event does not include tags', async () => {
+ await findTextarea().setValue('hello');
+
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(false);
+ });
+
+ it('should clear the form', async () => {
+ setDatetime();
+ await nextTick();
+
+ expect(findDatePicker().props('value')).toBe(mockInputDate);
+ expect(findHourInput().element.value).toBe('5');
+ expect(findMinuteInput().element.value).toBe('45');
+
+ wrapper.vm.clear();
+ await nextTick();
+
+ expect(findDatePicker().props('value')).toStrictEqual(new Date(fakeDate));
+ expect(findHourInput().element.value).toBe('0');
+ expect(findMinuteInput().element.value).toBe('0');
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ });
+ });
});
describe('form button behaviour', () => {
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 ba0527e5395..24653a23036 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
@@ -9,8 +9,8 @@ import { mockEvents } from './mock_data';
describe('IncidentTimelineEventList', () => {
let wrapper;
- const mountComponent = ({ propsData, provide } = {}) => {
- const { action, noteHtml, occurredAt } = mockEvents[0];
+ const mountComponent = ({ propsData, provide, mockEvent = mockEvents[0] } = {}) => {
+ const { action, noteHtml, occurredAt } = mockEvent;
wrapper = mountExtended(IncidentTimelineEventItem, {
propsData: {
action,
@@ -27,9 +27,10 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
- const findEventTag = () => wrapper.findComponent(GlBadge);
+ const findEventTags = () => wrapper.findAllComponents(GlBadge);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
+ const findEditButton = () => wrapper.findByText(timelineItemI18n.edit);
describe('template', () => {
beforeEach(() => {
@@ -69,15 +70,16 @@ 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' } });
+ describe.each([
+ { eventTags: [], expected: 0 },
+ { eventTags: ['Start time'], expected: 1 },
+ { eventTags: ['Start time', 'End time'], expected: 2 },
+ ])('timeline event tags', ({ eventTags, expected }) => {
+ it(`shows ${expected} badges when ${expected} tags are provided`, () => {
+ mountComponent({ propsData: { eventTags } });
- expect(findEventTag().exists()).toBe(true);
+ expect(findEventTags().exists()).toBe(Boolean(expected));
+ expect(findEventTags().length).toBe(eventTags.length);
});
});
@@ -87,6 +89,21 @@ describe('IncidentTimelineEventList', () => {
expect(findDeleteButton().exists()).toBe(false);
});
+ it('does not show edit item when event was system generated', () => {
+ const systemGeneratedMockEvent = {
+ ...mockEvents[0],
+ action: 'status',
+ };
+
+ mountComponent({
+ provide: { canUpdateTimelineEvent: true },
+ mockEvent: systemGeneratedMockEvent,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findEditButton().exists()).toBe(false);
+ });
+
it('shows dropdown and delete item when user has update permission', () => {
mountComponent({ provide: { canUpdateTimelineEvent: true } });
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 a7250e8ad0d..26fda877089 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
@@ -20,6 +20,7 @@ import {
timelineEventsEditEventError,
fakeDate,
fakeEventData,
+ fakeEventSaveData,
mockInputData,
} from './mock_data';
@@ -92,9 +93,7 @@ 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,
- );
+ expect(findItems().at(1).props('eventTags')).toBe(mockEvents[1].timelineEventTags.nodes);
});
it('formats dates correctly', () => {
@@ -123,20 +122,6 @@ 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 });
@@ -183,20 +168,20 @@ describe('IncidentTimelineEventList', () => {
});
const findEditEvent = () => wrapper.findComponent(EditTimelineEvent);
- const mockSaveData = { ...fakeEventData, ...mockInputData };
+ const mockHandleSaveEventData = { ...fakeEventData, ...mockInputData };
describe('editTimelineEvent', () => {
it('should call the mutation with the right variables', async () => {
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', mockHandleSaveEventData);
await waitForPromises();
expect(editResponseSpy).toHaveBeenCalledWith({
- input: mockSaveData,
+ input: fakeEventSaveData,
});
});
it('should close the form on successful addition', async () => {
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(findEditEvent().exists()).toBe(false);
@@ -217,7 +202,7 @@ describe('IncidentTimelineEventList', () => {
};
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
@@ -231,7 +216,7 @@ describe('IncidentTimelineEventList', () => {
};
editResponseSpy.mockRejectedValueOnce();
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
@@ -240,7 +225,7 @@ describe('IncidentTimelineEventList', () => {
it('should keep the form open on failed addition', async () => {
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(findEditEvent().exists()).toBe(true);
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
index 5bac1d6e7ad..63474070701 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -112,7 +112,9 @@ describe('TimelineEventsTab', () => {
await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
- expect(findTimelineEventsList().props('timelineEvents')).toHaveLength(3);
+ expect(findTimelineEventsList().props('timelineEvents')).toHaveLength(
+ timelineEventsQueryListResponse.data.project.incidentManagementTimelineEvents.nodes.length,
+ );
});
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js
new file mode 100644
index 00000000000..b39e96127c3
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js
@@ -0,0 +1,48 @@
+import { nextTick } from 'vue';
+import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import TimelineEventsTagsPopover from '~/issues/show/components/incidents/timeline_events_tags_popover.vue';
+
+describe('TimelineEventsTagsPopover component', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(TimelineEventsTagsPopover, {
+ stubs: {
+ GlPopover,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findQuestionIcon = () => wrapper.findComponent(GlIcon);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findDocumentationLink = () => findPopover().findComponent(GlLink);
+
+ describe('question icon', () => {
+ it('should open a popover with a link when hovered', async () => {
+ findQuestionIcon().vm.$emit('hover');
+ await nextTick();
+
+ expect(findPopover().exists()).toBe(true);
+ expect(findDocumentationLink().exists()).toBe(true);
+ });
+ });
+
+ describe('documentation link', () => {
+ it('redirects to a correct documentation page', async () => {
+ findQuestionIcon().vm.$emit('hover');
+ await nextTick();
+
+ expect(findDocumentationLink().attributes('href')).toBe(
+ helpPagePath('/ee/operations/incident_management/incident_timeline_events', {
+ anchor: 'incident-tags',
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index f0494591e95..75be17f9889 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -3,8 +3,10 @@ import {
displayAndLogError,
getEventIcon,
getUtcShiftedDate,
+ getPreviousEventTags,
} from '~/issues/show/components/incidents/utils';
import { createAlert } from '~/flash';
+import { mockTimelineEventTags } from './mock_data';
jest.mock('~/flash');
@@ -51,4 +53,20 @@ describe('incident utils', () => {
expect(shiftedDate > date).toBe(true);
});
});
+
+ describe('getPreviousEventTags', () => {
+ it('should return an empty array, when passed object contains no tags', () => {
+ const nodes = [];
+ const previousTags = getPreviousEventTags(nodes);
+
+ expect(previousTags.length).toBe(0);
+ });
+
+ it('should return an array of strings, when passed object containing tags', () => {
+ const previousTags = getPreviousEventTags(mockTimelineEventTags.nodes);
+ expect(previousTags.length).toBe(2);
+ expect(previousTags).toContain(mockTimelineEventTags.nodes[0].name);
+ expect(previousTags).toContain(mockTimelineEventTags.nodes[1].name);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
index 08f0338d41b..dd3c7c58380 100644
--- a/spec/frontend/issues/show/components/locked_warning_spec.js
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue';
describe('LockedWarning component', () => {
@@ -21,35 +21,32 @@ describe('LockedWarning component', () => {
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}`);
- });
- },
- );
+ describe.each([TYPE_ISSUE, TYPE_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/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
new file mode 100644
index 00000000000..d52f9d57453
--- /dev/null
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -0,0 +1,54 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
+import eventHub from '~/issues/show/event_hub';
+
+describe('TaskListItemActions component', () => {
+ let wrapper;
+
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0);
+ const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+
+ const mountComponent = () => {
+ const li = document.createElement('li');
+ li.dataset.sourcepos = '3:1-3:10';
+ li.appendChild(document.createElement('div'));
+ document.body.appendChild(li);
+
+ wrapper = shallowMount(TaskListItemActions, {
+ provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
+ attachTo: document.querySelector('div'),
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders dropdown', () => {
+ expect(findGlDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ right: true,
+ text: TaskListItemActions.i18n.taskActions,
+ textSrOnly: true,
+ });
+ });
+
+ it('emits event when `Convert to task` dropdown item is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ findConvertToTaskItem().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
+ });
+
+ it('emits event when `Delete` dropdown item is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ findDeleteItem().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
+ });
+});
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 68c2e3768c7..2980a6c33ee 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -3,11 +3,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import { initIssueApp } from '~/issues/show';
import * as parseData from '~/issues/show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createStore from '~/notes/stores';
import { appProps } from './mock_data/mock_data';
const mock = new MockAdapter(axios);
-mock.onGet().reply(200);
+mock.onGet().reply(HTTP_STATUS_OK);
jest.mock('~/lib/utils/poll');
@@ -18,7 +19,9 @@ const setupHTML = (initialData) => {
describe('Issue show index', () => {
describe('initIssueApp', () => {
- it('should initialize app with no potential XSS attack', async () => {
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/390368
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 909789b7a0f..9f0b6fb1148 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -59,6 +59,14 @@ export const appProps = {
publishedIncidentUrl,
};
+export const descriptionHtmlWithList = `
+ <ul data-sourcepos="1:1-3:8" dir="auto">
+ <li data-sourcepos="1:1-1:8">todo 1</li>
+ <li data-sourcepos="2:1-2:8">todo 2</li>
+ <li data-sourcepos="3:1-3:8">todo 3</li>
+ </ul>
+`;
+
export const descriptionHtmlWithCheckboxes = `
<ul dir="auto" class="task-list" data-sourcepos"3:1-5:12">
<li class="task-list-item" data-sourcepos="3:1-3:11">
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
index 603fb5cc2a6..e5041dd559b 100644
--- a/spec/frontend/issues/show/utils_spec.js
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -1,4 +1,8 @@
-import { convertDescriptionWithNewSort } from '~/issues/show/utils';
+import {
+ deleteTaskListItem,
+ convertDescriptionWithNewSort,
+ extractTaskTitleAndDescription,
+} from '~/issues/show/utils';
describe('app/assets/javascripts/issues/show/utils.js', () => {
describe('convertDescriptionWithNewSort', () => {
@@ -137,4 +141,270 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
});
});
+
+ describe('deleteTaskListItem', () => {
+ const description = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ /* The equivalent HTML for the above markdown
+ <ol data-sourcepos="3:1-21:17">
+ <li data-sourcepos="3:1-21:17">item 1
+ <ol data-sourcepos="4:4-21:17">
+ <li data-sourcepos="4:4-4:16">
+ <p data-sourcepos="4:7-4:16">item 2</p>
+ </li>
+ <li data-sourcepos="5:4-7:19">
+ <p data-sourcepos="5:7-5:16">item 3</p>
+ <ol data-sourcepos="6:7-7:19">
+ <li data-sourcepos="6:7-6:19">item 4</li>
+ <li data-sourcepos="7:7-7:19">item 5</li>
+ </ol>
+ </li>
+ <li data-sourcepos="8:4-11:0">
+ <p data-sourcepos="8:7-8:16">item 6</p>
+ <p data-sourcepos="10:7-10:20">paragraph text</p>
+ </li>
+ <li data-sourcepos="12:4-20:19">
+ <p data-sourcepos="12:7-12:16">item 7</p>
+ <p data-sourcepos="14:7-14:20">paragraph text</p>
+ <ol data-sourcepos="16:7-20:19">
+ <li data-sourcepos="16:7-19:0">
+ <p data-sourcepos="16:10-16:19">item 8</p>
+ <p data-sourcepos="18:10-18:23">paragraph text</p>
+ </li>
+ <li data-sourcepos="20:7-20:19">
+ <p data-sourcepos="20:10-20:19">item 9</p>
+ </li>
+ </ol>
+ </li>
+ <li data-sourcepos="21:4-21:17">
+ <p data-sourcepos="21:7-21:17">item 10</p>
+ </li>
+ </ol>
+ </li>
+ </ol>
+ */
+
+ it('deletes item with no children', () => {
+ const sourcepos = '4:4-4:14';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 2',
+ });
+ });
+
+ it('deletes deeply nested item with no children', () => {
+ const sourcepos = '6:7-6:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 4',
+ });
+ });
+
+ it('deletes item with children and moves sub-tasks up a level', () => {
+ const sourcepos = '5:4-7:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 3',
+ });
+ });
+
+ it('deletes item with associated paragraph text', () => {
+ const sourcepos = '8:4-11:0';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskDescription,
+ taskTitle: 'item 6',
+ });
+ });
+
+ it('deletes item with associated paragraph text and moves sub-tasks up a level', () => {
+ const sourcepos = '12:4-20:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskDescription,
+ taskTitle: 'item 7',
+ });
+ });
+ });
+
+ describe('extractTaskTitleAndDescription', () => {
+ const description = `A multi-line
+description`;
+
+ describe('when title is pure code block', () => {
+ const title = '`code block`';
+
+ it('moves the title to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: 'Untitled',
+ description: title,
+ });
+ });
+
+ it('moves the title to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: 'Untitled',
+ description: `${title}\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is too long', () => {
+ const title =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimus quia ratione nostrum ut adipisci.';
+ const expectedTitle =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimu';
+
+ it('moves the title beyond the character limit to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: expectedTitle,
+ description: 's quia ratione nostrum ut adipisci.',
+ });
+ });
+
+ it('moves the title beyond the character limit to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: expectedTitle,
+ description: `s quia ratione nostrum ut adipisci.\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is fine', () => {
+ const title = 'A fine title';
+
+ it('uses the title with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({ title });
+ });
+
+ it('uses the title and description with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({ title, description });
+ });
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index 21636017f10..e2a14a9102f 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -104,9 +104,11 @@ describe('JiraConnect API', () => {
response = await makeRequest();
expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, {
+ headers: {},
params: {
page: mockPage,
per_page: mockPerPage,
+ search: undefined,
},
});
expect(response.data).toEqual(mockResponse);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index f1fc5e4d90b..97038a2a231 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -27,6 +27,7 @@ jest.mock('~/jira_connect/subscriptions/api', () => {
});
const mockGroupsPath = '/groups';
+const mockAccessToken = '123';
describe('GroupsList', () => {
let wrapper;
@@ -39,6 +40,9 @@ describe('GroupsList', () => {
provide: {
groupsPath: mockGroupsPath,
},
+ computed: {
+ accessToken: () => mockAccessToken,
+ },
...options,
}),
);
@@ -148,11 +152,15 @@ describe('GroupsList', () => {
});
it('calls `fetchGroups` with search term', () => {
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: mockSearchTeam,
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 1,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: mockSearchTeam,
+ },
+ mockAccessToken,
+ );
});
it('disables GroupListItems', () => {
@@ -222,11 +230,15 @@ describe('GroupsList', () => {
findSearchBox().vm.$emit('input', newSearch);
if (shouldSearch) {
- expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: expectedSearchValue,
- });
+ expect(fetchGroups).toHaveBeenCalledWith(
+ mockGroupsPath,
+ {
+ page: 1,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchValue,
+ },
+ mockAccessToken,
+ );
} else {
expect(fetchGroups).not.toHaveBeenCalled();
}
@@ -257,11 +269,15 @@ describe('GroupsList', () => {
});
it('should load results for page 2', () => {
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 2,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: '',
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 2,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: '',
+ },
+ mockAccessToken,
+ );
});
it.each`
@@ -274,11 +290,15 @@ describe('GroupsList', () => {
const searchBox = findSearchBox();
searchBox.vm.$emit('input', searchTerm);
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: expectedPage,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: expectedSearchTerm,
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: expectedPage,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchTerm,
+ },
+ mockAccessToken,
+ );
},
);
});
@@ -324,11 +344,15 @@ describe('GroupsList', () => {
const paginationEl = findPagination();
paginationEl.vm.$emit('input', 2);
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 2,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: '',
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 2,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: '',
+ },
+ mockAccessToken,
+ );
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
index c12a45b2f41..b27eba6b040 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -15,7 +15,7 @@ describe('SignInPage', () => {
const createComponent = ({
props = {},
jiraConnectOauthEnabled,
- jiraConnectOauthSelfManagedEnabled,
+ publicKeyStorageEnabled,
} = {}) => {
store = createStore();
@@ -24,10 +24,13 @@ describe('SignInPage', () => {
provide: {
glFeatures: {
jiraConnectOauth: jiraConnectOauthEnabled,
- jiraConnectOauthSelfManaged: jiraConnectOauthSelfManagedEnabled,
},
},
- propsData: { hasSubscriptions: false, ...props },
+ propsData: {
+ hasSubscriptions: false,
+ publicKeyStorageEnabled,
+ ...props,
+ },
});
};
@@ -36,47 +39,23 @@ describe('SignInPage', () => {
});
it.each`
- jiraConnectOauthEnabled | jiraConnectOauthSelfManagedEnabled | shouldRenderDotCom | shouldRenderMultiversion
- ${false} | ${false} | ${true} | ${false}
- ${false} | ${true} | ${true} | ${false}
- ${true} | ${false} | ${true} | ${false}
- ${true} | ${true} | ${false} | ${true}
+ jiraConnectOauthEnabled | publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion
+ ${false} | ${true} | ${true} | ${false}
+ ${false} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true}
+ ${true} | ${false} | ${true} | ${false}
`(
- 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled and jiraConnectOauthSelfManaged is $jiraConnectOauthSelfManagedEnabled',
+ 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled',
({
jiraConnectOauthEnabled,
- jiraConnectOauthSelfManagedEnabled,
+ publicKeyStorageEnabled,
shouldRenderDotCom,
shouldRenderMultiversion,
}) => {
- createComponent({ jiraConnectOauthEnabled, jiraConnectOauthSelfManagedEnabled });
+ createComponent({ jiraConnectOauthEnabled, publicKeyStorageEnabled });
expect(findSignInGitlabCom().exists()).toBe(shouldRenderDotCom);
expect(findSignInGitabMultiversion().exists()).toBe(shouldRenderMultiversion);
},
);
-
- describe('when jiraConnectOauthSelfManaged is false', () => {
- beforeEach(() => {
- createComponent({ jiraConnectOauthSelfManaged: false, props: { hasSubscriptions: true } });
- });
-
- it('renders SignInGitlabCom with correct props', () => {
- expect(findSignInGitlabCom().props()).toEqual({ hasSubscriptions: true });
- });
-
- describe('when error event is emitted', () => {
- it('emits another error event', () => {
- findSignInGitlabCom().vm.$emit('error');
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('when sign-in-oauth event is emitted', () => {
- it('emits another sign-in-oauth event', () => {
- findSignInGitlabCom().vm.$emit('sign-in-oauth');
- expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([]);
- });
- });
- });
});
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 98f1979db1b..cefedcd82fb 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -15,7 +15,10 @@ import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
import createStore from '~/jobs/store';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { MANUAL_STATUS } from '~/jobs/constants';
import job from '../../mock_data';
+import { mockPendingJobData } from './mock_data';
describe('Job App', () => {
Vue.use(Vuex);
@@ -48,8 +51,8 @@ describe('Job App', () => {
};
const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => {
- mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
- mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData);
+ mock.onGet(initSettings.endpoint).replyOnce(HTTP_STATUS_OK, { ...job, ...jobData });
+ mock.onGet(`${initSettings.pagePath}/trace.json`).reply(HTTP_STATUS_OK, jobLogData);
const asyncInit = store.dispatch('init', initSettings);
@@ -310,4 +313,29 @@ describe('Job App', () => {
expect(findJobLog().exists()).toBe(true);
});
});
+
+ describe('job log polling', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it('should poll job log by default', async () => {
+ await setupAndMount({
+ jobData: mockPendingJobData,
+ });
+
+ expect(store.dispatch).toHaveBeenCalledWith('fetchJobLog');
+ });
+
+ it('should NOT poll job log for manual variables form empty state', async () => {
+ const manualPendingJobData = mockPendingJobData;
+ manualPendingJobData.status.group = MANUAL_STATUS;
+
+ await setupAndMount({
+ jobData: manualPendingJobData,
+ });
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('fetchJobLog');
+ });
+ });
});
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 3040570df19..a5b3b0e3b47 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -4,9 +4,10 @@ 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 { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { GRAPHQL_ID_TYPES } from '~/jobs/constants';
import waitForPromises from 'helpers/wait_for_promises';
+import { redirectTo } from '~/lib/utils/url_utility';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
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';
@@ -21,6 +22,11 @@ import {
const localVue = createLocalVue();
localVue.use(VueApollo);
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
const defaultProvide = {
projectPath: mockFullPath,
};
@@ -146,7 +152,7 @@ describe('Manual Variables Form', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: retryJobMutation,
variables: {
- id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId),
+ id: convertToGraphQLId(TYPENAME_CI_BUILD, mockId),
variables: [
{
key: 'new key',
@@ -156,6 +162,15 @@ describe('Manual Variables Form', () => {
},
});
});
+
+ // redirect to job after initial trigger assertion will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+ it('redirects to job properly after rerun', async () => {
+ findRerunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobMutationData.data.jobRetry.job.webPath);
+ });
});
describe('updating variables in UI', () => {
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
index 9596e859475..8a838acca7a 100644
--- a/spec/frontend/jobs/components/job/mock_data.js
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -74,3 +74,25 @@ export const mockJobMutationData = {
},
},
};
+
+export const mockPendingJobData = {
+ has_trace: false,
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ illustration: {
+ image: 'path',
+ size: '340',
+ title: '',
+ content: '',
+ },
+ action: {
+ button_title: 'Retry job',
+ method: 'post',
+ path: '/path',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 7cc008f332d..55fe534aa3b 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -37,6 +37,7 @@ describe('Job actions cell', () => {
const cancelableJob = findMockJob('cancelable');
const playableJob = findMockJob('playable');
const retryableJob = findMockJob('retryable');
+ const failedJob = findMockJob('failed');
const scheduledJob = findMockJob('scheduled');
const jobWithArtifact = findMockJob('with_artifact');
const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest);
@@ -79,10 +80,6 @@ describe('Job actions cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the artifacts download button with correct link', () => {
createComponent(jobWithArtifact);
@@ -191,6 +188,20 @@ describe('Job actions cell', () => {
expect(button().props('disabled')).toBe(true);
});
+ describe('Retry button title', () => {
+ it('displays retry title when job has failed and is retryable', () => {
+ createComponent(failedJob);
+
+ expect(findRetryButton().attributes('title')).toBe('Retry');
+ });
+
+ it('displays run again title when job has passed and is retryable', () => {
+ createComponent(retryableJob);
+
+ expect(findRetryButton().attributes('title')).toBe('Run again');
+ });
+ });
+
describe('Scheduled Jobs', () => {
const today = () => new Date('2021-08-31');
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index 0d11c4d56bf..73a158d52d8 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -30,6 +30,7 @@ import {
import * as types from '~/jobs/store/mutation_types';
import state from '~/jobs/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Job State actions', () => {
let mockedState;
@@ -112,7 +113,9 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJob and receiveJobSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`)
+ .replyOnce(HTTP_STATUS_OK, { id: 121212, name: 'karma' });
return testAction(
fetchJob,
@@ -134,7 +137,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJob and receiveJobError', () => {
@@ -214,7 +217,7 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', () => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
});
@@ -252,7 +255,7 @@ describe('Job State actions', () => {
complete: false,
};
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, jobLogPayload);
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, jobLogPayload);
});
it('dispatches startPollingJobLog', () => {
@@ -288,7 +291,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJobLog and receiveJobLogError', () => {
@@ -424,9 +427,10 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJobsForStage and receiveJobsForStageSuccess', () => {
- mock
- .onGet(`${TEST_HOST}/jobs.json`)
- .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
+ mock.onGet(`${TEST_HOST}/jobs.json`).replyOnce(HTTP_STATUS_OK, {
+ latest_statuses: [{ id: 121212, name: 'build' }],
+ retried: [],
+ });
return testAction(
fetchJobsForStage,
@@ -449,7 +453,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/jobs.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJobsForStage and receiveJobsForStageError', () => {
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index 8953e3cbcd8..97913c20229 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -6,6 +6,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import { stubComponent } from 'helpers/stub_component';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PromoteLabelModal from '~/labels/components/promote_label_modal.vue';
import eventHub from '~/labels/event_hub';
@@ -66,7 +67,7 @@ describe('Promote label modal', () => {
it('redirects when a label is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
- axiosMock.onPost(labelMockData.url).reply(200, { url: responseURL });
+ axiosMock.onPost(labelMockData.url).reply(HTTP_STATUS_OK, { url: responseURL });
wrapper.findComponent(GlModal).vm.$emit('primary');
@@ -85,8 +86,10 @@ describe('Promote label modal', () => {
it('displays an error if promoting a label failed', async () => {
const dummyError = new Error('promoting label failed');
- dummyError.response = { status: 500 };
- axiosMock.onPost(labelMockData.url).reply(500, { error: dummyError });
+ dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
+ axiosMock
+ .onPost(labelMockData.url)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { error: dummyError });
wrapper.findComponent(GlModal).vm.$emit('primary');
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
index effb71c2775..7f6fb138d89 100644
--- a/spec/frontend/language_switcher/components/app_spec.js
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -28,7 +28,7 @@ describe('<LanguageSwitcher />', () => {
wrapper.destroy();
});
- const getPreferredLanguage = () => wrapper.find('.gl-dropdown-button-text').text();
+ const getPreferredLanguage = () => wrapper.find('.gl-new-dropdown-button-text').text();
const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
const findFooter = () => wrapper.findByTestId('footer');
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index e0b6c7119f9..190114606ec 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -60,7 +60,6 @@ describe('LazyLoader', () => {
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(execImmediately);
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(LazyLoader, 'loadImage');
mockLoadEvent();
});
@@ -106,7 +105,6 @@ describe('LazyLoader', () => {
trigger(img);
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
@@ -121,7 +119,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
@@ -131,7 +128,6 @@ describe('LazyLoader', () => {
lazyLoader.register();
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -143,7 +139,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -158,7 +153,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
diff --git a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json
new file mode 100644
index 00000000000..a0d67885dad
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json
@@ -0,0 +1,3089 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"}) @persist": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
new file mode 100644
index 00000000000..c0651517986
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
@@ -0,0 +1,3091 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "__persist": true,
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"}) @persist": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "__persist": true,
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json b/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json
new file mode 100644
index 00000000000..f30461f63db
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json
@@ -0,0 +1,3089 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"})": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/persist_link_spec.js b/spec/frontend/lib/apollo/persist_link_spec.js
new file mode 100644
index 00000000000..ddb861bcee0
--- /dev/null
+++ b/spec/frontend/lib/apollo/persist_link_spec.js
@@ -0,0 +1,74 @@
+/* eslint-disable no-underscore-dangle */
+import { gql, execute, ApolloLink, Observable } from '@apollo/client/core';
+import { testApolloLink } from 'helpers/test_apollo_link';
+import { getPersistLink } from '~/lib/apollo/persist_link';
+
+const DEFAULT_QUERY = gql`
+ query {
+ foo {
+ bar
+ }
+ }
+`;
+
+const QUERY_WITH_DIRECTIVE = gql`
+ query {
+ foo @persist {
+ bar
+ }
+ }
+`;
+
+const QUERY_WITH_PERSIST_FIELD = gql`
+ query {
+ foo @persist {
+ bar
+ __persist
+ }
+ }
+`;
+
+const terminatingLink = new ApolloLink(() => Observable.of({ data: { foo: { bar: 1 } } }));
+
+describe('~/lib/apollo/persist_link', () => {
+ let subscription;
+
+ afterEach(() => {
+ if (subscription) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it('removes `@persist` directive from the operation', async () => {
+ const operation = await testApolloLink(getPersistLink(), {}, QUERY_WITH_DIRECTIVE);
+ const { selections } = operation.query.definitions[0].selectionSet;
+
+ expect(selections[0].directives).toEqual([]);
+ });
+
+ it('removes `__persist` fields from the operation with `@persist` directive', async () => {
+ const operation = await testApolloLink(getPersistLink(), {}, QUERY_WITH_PERSIST_FIELD);
+
+ const { selections } = operation.query.definitions[0].selectionSet;
+ const childFields = selections[0].selectionSet.selections;
+
+ expect(childFields).toHaveLength(1);
+ expect(childFields.some((field) => field.name.value === '__persist')).toBe(false);
+ });
+
+ it('decorates the response with `__persist: true` is there is `__persist` field in the query', async () => {
+ const link = getPersistLink().concat(terminatingLink);
+
+ subscription = execute(link, { query: QUERY_WITH_PERSIST_FIELD }).subscribe(({ data }) => {
+ expect(data.foo.__persist).toBe(true);
+ });
+ });
+
+ it('does not decorate the response with `__persist: true` is there if query is not persistent', async () => {
+ const link = getPersistLink().concat(terminatingLink);
+
+ subscription = execute(link, { query: DEFAULT_QUERY }).subscribe(({ data }) => {
+ expect(data.foo.__persist).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/frontend/lib/apollo/persistence_mapper_spec.js b/spec/frontend/lib/apollo/persistence_mapper_spec.js
new file mode 100644
index 00000000000..2efe28d2ca7
--- /dev/null
+++ b/spec/frontend/lib/apollo/persistence_mapper_spec.js
@@ -0,0 +1,163 @@
+import { persistenceMapper } from '~/lib/apollo/persistence_mapper';
+import NON_PERSISTED_CACHE from './mock_data/non_persisted_cache.json';
+import CACHE_WITH_PERSIST_DIRECTIVE from './mock_data/cache_with_persist_directive.json';
+import CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS from './mock_data/cache_with_persist_directive_and_field.json';
+
+describe('lib/apollo/persistence_mapper', () => {
+ it('returns only empty root query if `@persist` directive or `__persist` field is not present', async () => {
+ const persistedData = await persistenceMapper(JSON.stringify(NON_PERSISTED_CACHE));
+
+ expect(JSON.parse(persistedData)).toEqual({ ROOT_QUERY: { __typename: 'Query' } });
+ });
+
+ it('returns root query with one `project` field if only `@persist` directive is present', async () => {
+ const persistedData = await persistenceMapper(JSON.stringify(CACHE_WITH_PERSIST_DIRECTIVE));
+
+ expect(JSON.parse(persistedData)).toEqual({
+ ROOT_QUERY: {
+ __typename: 'Query',
+ 'project({"fullPath":"flightjs/Flight"}) @persist': {
+ __ref: 'Project:gid://gitlab/Project/6',
+ },
+ },
+ 'Project:gid://gitlab/Project/6': { __typename: 'Project', id: 'gid://gitlab/Project/6' },
+ });
+ });
+
+ it('returns root query nested fields that contain `__persist` field if `@persist` directive is present', async () => {
+ const persistedData = await persistenceMapper(
+ JSON.stringify(CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS),
+ );
+
+ expect(JSON.parse(persistedData)).toEqual({
+ ROOT_QUERY: {
+ __typename: 'Query',
+ 'project({"fullPath":"flightjs/Flight"}) @persist': {
+ __ref: 'Project:gid://gitlab/Project/6',
+ },
+ },
+ 'Project:gid://gitlab/Project/6': {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ 'issues({"after":null,"before":"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ","includeSubepics":true,"last":20,"sort":"UPDATED_DESC","state":"opened","types":["ISSUE","INCIDENT","TEST_CASE","TASK"]})': {
+ __typename: 'IssueConnection',
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor:
+ 'eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9',
+ endCursor:
+ 'eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ',
+ },
+ nodes: [
+ {
+ __ref: 'Issue:gid://gitlab/Issue/483',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1585',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1584',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1583',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1582',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1581',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1580',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1579',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1578',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1577',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1576',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1575',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1574',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1573',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1572',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1571',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1570',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1569',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1568',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1567',
+ },
+ ],
+ },
+ },
+ 'Issue:gid://gitlab/Issue/483': {
+ __typename: 'Issue',
+ __persist: true,
+ id: 'gid://gitlab/Issue/483',
+ iid: '31',
+ confidential: false,
+ createdAt: '2022-09-11T15:24:16Z',
+ downvotes: 1,
+ dueDate: null,
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: 1,
+ moved: false,
+ state: 'opened',
+ title: 'Instigate the Incident!',
+ updatedAt: '2023-01-10T12:36:54Z',
+ closedAt: null,
+ upvotes: 0,
+ userDiscussionsCount: 2,
+ webPath: '/flightjs/Flight/-/issues/31',
+ webUrl: 'https://gdk.test:3443/flightjs/Flight/-/issues/31',
+ type: 'INCIDENT',
+ assignees: {
+ __typename: 'UserCoreConnection',
+ nodes: [],
+ },
+ author: {
+ __ref: 'UserCore:gid://gitlab/User/1',
+ },
+ labels: {
+ __typename: 'LabelConnection',
+ nodes: [],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ __typename: 'TaskCompletionStatus',
+ completedCount: 0,
+ count: 0,
+ },
+ blockingCount: 0,
+ healthStatus: null,
+ weight: null,
+ },
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js
index d4b95172d18..338302642ff 100644
--- a/spec/frontend/lib/utils/ajax_cache_spec.js
+++ b/spec/frontend/lib/utils/ajax_cache_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AjaxCache', () => {
const dummyEndpoint = '/AjaxCache/dummyEndpoint';
@@ -102,7 +103,7 @@ describe('AjaxCache', () => {
});
it('stores and returns data from Ajax call if cache is empty', () => {
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return AjaxCache.retrieve(dummyEndpoint).then((data) => {
expect(data).toEqual(dummyResponse);
@@ -111,7 +112,7 @@ describe('AjaxCache', () => {
});
it('makes no Ajax call if request is pending', () => {
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return Promise.all([
AjaxCache.retrieve(dummyEndpoint),
@@ -148,7 +149,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return Promise.all([
AjaxCache.retrieve(dummyEndpoint),
diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
index b972f669ac4..78eef205b49 100644
--- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
+++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
@@ -1,5 +1,6 @@
import { ApolloLink, Observable } from '@apollo/client/core';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('StartupJSLink', () => {
const FORWARDED_RESPONSE = { data: 'FORWARDED_RESPONSE' };
@@ -37,7 +38,7 @@ describe('StartupJSLink', () => {
let startupLink;
let link;
- function mockFetchCall(status = 200, response = STARTUP_JS_RESPONSE) {
+ function mockFetchCall(status = HTTP_STATUS_OK, response = STARTUP_JS_RESPONSE) {
const p = {
ok: status >= 200 && status < 300,
status,
@@ -175,7 +176,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(404),
+ fetchCall: mockFetchCall(HTTP_STATUS_NOT_FOUND),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -209,7 +210,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(200, ERROR_RESPONSE),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, ERROR_RESPONSE),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -226,7 +227,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(200, { 'no-data': 'yay' }),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, { 'no-data': 'yay' }),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -339,7 +340,7 @@ describe('StartupJSLink', () => {
variables: { id: 3 },
},
{
- fetchCall: mockFetchCall(200, STARTUP_JS_RESPONSE_TWO),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, STARTUP_JS_RESPONSE_TWO),
query: STARTUP_JS_QUERY_TWO,
variables: { id: 3 },
},
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
index e12bf725560..4471b781446 100644
--- a/spec/frontend/lib/utils/axios_startup_calls_spec.js
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import setupAxiosStartupCalls from '~/lib/utils/axios_startup_calls';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('setupAxiosStartupCalls', () => {
const AXIOS_RESPONSE = { text: 'AXIOS_RESPONSE' };
@@ -31,9 +32,9 @@ describe('setupAxiosStartupCalls', () => {
beforeEach(() => {
window.gl = {};
mock = new MockAdapter(axios);
- mock.onGet('/non-startup').reply(200, AXIOS_RESPONSE);
- mock.onGet('/startup').reply(200, AXIOS_RESPONSE);
- mock.onGet('/startup-failing').reply(200, AXIOS_RESPONSE);
+ mock.onGet('/non-startup').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onGet('/startup').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onGet('/startup-failing').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
});
afterEach(() => {
@@ -52,10 +53,10 @@ describe('setupAxiosStartupCalls', () => {
beforeEach(() => {
window.gl.startup_calls = {
'/startup': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
'/startup-failing': {
- fetchCall: mockFetchCall(400),
+ fetchCall: mockFetchCall(HTTP_STATUS_BAD_REQUEST),
},
};
setupAxiosStartupCalls(axios);
@@ -80,7 +81,7 @@ describe('setupAxiosStartupCalls', () => {
const { headers, data, status, statusText } = await axios.get('/startup');
expect(headers).toEqual({ 'content-type': 'application/json' });
- expect(status).toBe(200);
+ expect(status).toBe(HTTP_STATUS_OK);
expect(statusText).toBe('MOCK-FETCH 200');
expect(data).toEqual(STARTUP_JS_RESPONSE);
expect(data).not.toEqual(AXIOS_RESPONSE);
@@ -126,7 +127,7 @@ describe('setupAxiosStartupCalls', () => {
it('removes GitLab Base URL from startup call', async () => {
window.gl.startup_calls = {
'/startup': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
};
setupAxiosStartupCalls(axios);
@@ -139,7 +140,7 @@ describe('setupAxiosStartupCalls', () => {
it('sorts the params in the requested API url', async () => {
window.gl.startup_calls = {
'/startup?alpha=true&bravo=true': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
};
setupAxiosStartupCalls(axios);
diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js
index 1585a38ae86..2656fb1d648 100644
--- a/spec/frontend/lib/utils/axios_utils_spec.js
+++ b/spec/frontend/lib/utils/axios_utils_spec.js
@@ -3,14 +3,15 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('axios_utils', () => {
let mock;
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
- mock.onAny('/ok').reply(200);
- mock.onAny('/err').reply(500);
+ mock.onAny('/ok').reply(HTTP_STATUS_OK);
+ mock.onAny('/err').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
// eslint-disable-next-line jest/no-standalone-expect
expect(axios.countActiveRequests()).toBe(0);
});
@@ -27,8 +28,8 @@ describe('axios_utils', () => {
return axios.waitForAll().finally(() => {
expect(handler).toHaveBeenCalledTimes(2);
- expect(handler.mock.calls[0][0].status).toBe(200);
- expect(handler.mock.calls[1][0].response.status).toBe(500);
+ expect(handler.mock.calls[0][0].status).toBe(HTTP_STATUS_OK);
+ expect(handler.mock.calls[1][0].response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 08ba78cddff..7b068f7d248 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,5 +1,4 @@
import * as commonUtils from '~/lib/utils/common_utils';
-import setWindowLocation from 'helpers/set_window_location_helper';
describe('common_utils', () => {
describe('getPagePath', () => {
@@ -623,6 +622,23 @@ describe('common_utils', () => {
milestones: ['12.3', '12.4'],
},
},
+ convertObjectPropsToLowerCase: {
+ obj: {
+ 'Project-Name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'License-Type': 'MIT',
+ 'Mile-Stones': ['12.3', '12.4'],
+ },
+ objNested: {
+ 'Project-Name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'License-Type': 'MIT',
+ 'Tech-Stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'Mile-Stones': ['12.3', '12.4'],
+ },
+ },
};
describe('convertObjectProps', () => {
@@ -638,6 +654,7 @@ describe('common_utils', () => {
${'convertObjectProps'} | ${mockObjects.convertObjectProps.obj} | ${mockObjects.convertObjectProps.objNested}
${'convertObjectPropsToCamelCase'} | ${mockObjects.convertObjectPropsToCamelCase.obj} | ${mockObjects.convertObjectPropsToCamelCase.objNested}
${'convertObjectPropsToSnakeCase'} | ${mockObjects.convertObjectPropsToSnakeCase.obj} | ${mockObjects.convertObjectPropsToSnakeCase.objNested}
+ ${'convertObjectPropsToLowerCase'} | ${mockObjects.convertObjectPropsToLowerCase.obj} | ${mockObjects.convertObjectPropsToLowerCase.objNested}
`('$functionName', ({ functionName, mockObj, mockObjNested }) => {
const testFunction =
functionName === 'convertObjectProps'
@@ -671,6 +688,12 @@ describe('common_utils', () => {
absolute_web_url: 'https://gitlab.com/gitlab-org/',
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
expect(testFunction(mockObj)).toEqual(expected[functionName]);
@@ -711,6 +734,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
expect(testFunction(mockObjNested)).toEqual(expected[functionName]);
@@ -752,6 +784,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'frontend-framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
it('converts nested objects', () => {
@@ -802,6 +843,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const dropKeys = {
@@ -846,12 +896,20 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'tech-stack': {
+ 'frontend-framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const dropKeys = {
convertObjectProps: ['group_name', 'database'],
convertObjectPropsToCamelCase: ['group_name', 'database'],
convertObjectPropsToSnakeCase: ['groupName', 'database'],
+ convertObjectPropsToLowerCase: ['Group-Name', 'License-Type'],
};
expect(
@@ -899,12 +957,22 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const ignoreKeyNames = {
convertObjectProps: ['group_name'],
convertObjectPropsToCamelCase: ['group_name'],
convertObjectPropsToSnakeCase: ['groupName'],
+ convertObjectPropsToLowerCase: ['Group-Name'],
};
expect(
@@ -949,12 +1017,22 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const ignoreKeyNames = {
convertObjectProps: ['group_name', 'frontend_framework'],
convertObjectPropsToCamelCase: ['group_name', 'frontend_framework'],
convertObjectPropsToSnakeCase: ['groupName', 'frontendFramework'],
+ convertObjectPropsToLowerCase: ['Frontend-Framework'],
};
expect(
@@ -1070,35 +1148,4 @@ 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/favicon_ci_spec.js b/spec/frontend/lib/utils/favicon_ci_spec.js
index e35b008b862..be647d98f1a 100644
--- a/spec/frontend/lib/utils/favicon_ci_spec.js
+++ b/spec/frontend/lib/utils/favicon_ci_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
import { setCiStatusFavicon } from '~/lib/utils/favicon_ci';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/favicon');
@@ -28,7 +29,7 @@ describe('~/lib/utils/favicon_ci', () => {
`(
'with response=$response',
async ({ response, setFaviconOverlayCalls, resetFaviconCalls }) => {
- mock.onGet(TEST_URL).replyOnce(200, response);
+ mock.onGet(TEST_URL).replyOnce(HTTP_STATUS_OK, response);
expect(setFaviconOverlay).not.toHaveBeenCalled();
expect(resetFavicon).not.toHaveBeenCalled();
@@ -41,7 +42,7 @@ describe('~/lib/utils/favicon_ci', () => {
);
it('with error', async () => {
- mock.onGet(TEST_URL).replyOnce(500);
+ mock.onGet(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await expect(setCiStatusFavicon(TEST_URL)).rejects.toEqual(expect.any(Error));
expect(resetFavicon).toHaveBeenCalled();
diff --git a/spec/frontend/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js
index db1f174703b..59839862504 100644
--- a/spec/frontend/lib/utils/icon_utils_spec.js
+++ b/spec/frontend/lib/utils/icon_utils_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils';
describe('Icon utils', () => {
@@ -30,7 +31,7 @@ describe('Icon utils', () => {
describe('when the icons can be loaded', () => {
beforeEach(() => {
- axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons);
+ axiosMock.onGet(gon.sprite_icons).reply(HTTP_STATUS_OK, mockIcons);
});
it('extracts svg icon path content from sprite icons', () => {
@@ -50,11 +51,11 @@ describe('Icon utils', () => {
beforeEach(() => {
axiosMock
.onGet(gon.sprite_icons)
- .replyOnce(500)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
.onGet(gon.sprite_icons)
- .replyOnce(500)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
.onGet(gon.sprite_icons)
- .reply(200, mockIcons);
+ .reply(HTTP_STATUS_OK, mockIcons);
});
it('returns null', () => {
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 94a5f5385b7..63eeb54e850 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -1,5 +1,9 @@
import waitForPromises from 'helpers/wait_for_promises';
-import { successCodes } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ successCodes,
+} from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
describe('Poll', () => {
@@ -51,7 +55,7 @@ describe('Poll', () => {
});
it('calls the success callback when no header for interval is provided', () => {
- mockServiceCall({ status: 200 });
+ mockServiceCall({ status: HTTP_STATUS_OK });
setup();
return waitForAllCallsToFinish(1, () => {
@@ -61,7 +65,7 @@ describe('Poll', () => {
});
it('calls the error callback when the http request returns an error', () => {
- mockServiceCall({ status: 500 }, true);
+ mockServiceCall({ status: HTTP_STATUS_INTERNAL_SERVER_ERROR }, true);
setup();
return waitForAllCallsToFinish(1, () => {
@@ -82,7 +86,7 @@ describe('Poll', () => {
});
it('should call the success callback when the interval header is -1', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': -1 } });
return setup().then(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
@@ -118,7 +122,7 @@ describe('Poll', () => {
describe('with delayed initial request', () => {
it('delays the first request', async () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -147,7 +151,7 @@ describe('Poll', () => {
describe('stop', () => {
it('stops polling when method is called', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -173,7 +177,7 @@ describe('Poll', () => {
describe('enable', () => {
it('should enable polling upon a response', () => {
- mockServiceCall({ status: 200 });
+ mockServiceCall({ status: HTTP_STATUS_OK });
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -183,7 +187,7 @@ describe('Poll', () => {
Polling.enable({
data: { page: 4 },
- response: { status: 200, headers: { 'poll-interval': 1 } },
+ response: { status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } },
});
return waitForAllCallsToFinish(1, () => {
@@ -198,7 +202,7 @@ describe('Poll', () => {
describe('restart', () => {
it('should restart polling when its called', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js
index da9cc5c6f3c..8ca4dfc9340 100644
--- a/spec/frontend/lib/utils/rails_ujs_spec.js
+++ b/spec/frontend/lib/utils/rails_ujs_spec.js
@@ -1,5 +1,6 @@
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
beforeAll(async () => {
// @rails/ujs expects jQuery.ajaxPrefilter to exist if jQuery exists at
@@ -20,7 +21,7 @@ function mockXHRResponse({ responseText, responseContentType } = {}) {
jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() {
Object.defineProperties(this, {
readyState: { value: XMLHttpRequest.DONE },
- status: { value: 200 },
+ status: { value: HTTP_STATUS_OK },
response: { value: responseText },
});
this.onreadystatechange();
diff --git a/spec/frontend/lib/utils/scroll_utils_spec.js b/spec/frontend/lib/utils/scroll_utils_spec.js
new file mode 100644
index 00000000000..d42e25b929c
--- /dev/null
+++ b/spec/frontend/lib/utils/scroll_utils_spec.js
@@ -0,0 +1,21 @@
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+
+describe('isScrolledToBottom', () => {
+ const setScrollGetters = (getters) => {
+ Object.entries(getters).forEach(([name, value]) => {
+ jest.spyOn(Element.prototype, name, 'get').mockReturnValue(value);
+ });
+ };
+
+ it.each`
+ context | scrollTop | scrollHeight | result
+ ${'returns false when not scrolled to bottom'} | ${0} | ${2000} | ${false}
+ ${'returns true when scrolled to bottom'} | ${1000} | ${2000} | ${true}
+ ${'returns true when scrolled to bottom with subpixel precision'} | ${999.25} | ${2000} | ${true}
+ ${'returns true when cannot scroll'} | ${0} | ${500} | ${true}
+ `('$context', ({ scrollTop, scrollHeight, result }) => {
+ setScrollGetters({ scrollTop, clientHeight: 1000, scrollHeight });
+
+ expect(isScrolledToBottom()).toBe(result);
+ });
+});
diff --git a/spec/frontend/lib/utils/select2_utils_spec.js b/spec/frontend/lib/utils/select2_utils_spec.js
deleted file mode 100644
index 6d601dd5ad1..00000000000
--- a/spec/frontend/lib/utils/select2_utils_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import { setHTMLFixture } from 'helpers/fixtures';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import { select2AxiosTransport } from '~/lib/utils/select2_utils';
-
-import 'select2/select2';
-
-const TEST_URL = '/test/api/url';
-const TEST_SEARCH_DATA = { extraSearch: 'test' };
-const TEST_DATA = [{ id: 1 }];
-const TEST_SEARCH = 'FOO';
-
-describe('lib/utils/select2_utils', () => {
- let mock;
- let resultsSpy;
-
- beforeEach(() => {
- setHTMLFixture('<div><input id="root" /></div>');
-
- mock = new MockAdapter(axios);
-
- resultsSpy = jest.fn().mockReturnValue({ results: [] });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- const setupSelect2 = (input) => {
- input.select2({
- ajax: {
- url: TEST_URL,
- quietMillis: 250,
- transport: select2AxiosTransport,
- data(search, page) {
- return {
- search,
- page,
- ...TEST_SEARCH_DATA,
- };
- },
- results: resultsSpy,
- },
- });
- };
-
- const setupSelect2AndSearch = async () => {
- const $input = $('#root');
-
- setupSelect2($input);
-
- $input.select2('search', TEST_SEARCH);
-
- jest.runOnlyPendingTimers();
- await waitForPromises();
- };
-
- describe('select2AxiosTransport', () => {
- it('uses axios to make request', async () => {
- // setup mock response
- const replySpy = jest.fn();
- mock.onGet(TEST_URL).reply((...args) => replySpy(...args));
-
- await setupSelect2AndSearch();
-
- expect(replySpy).toHaveBeenCalledWith(
- expect.objectContaining({
- url: TEST_URL,
- method: 'get',
- params: {
- page: 1,
- search: TEST_SEARCH,
- ...TEST_SEARCH_DATA,
- },
- }),
- );
- });
-
- it.each`
- headers | pagination
- ${{}} | ${{ more: false }}
- ${{ 'X-PAGE': '1', 'x-next-page': 2 }} | ${{ more: true }}
- `(
- 'passes results and pagination to results callback, with headers=$headers',
- async ({ headers, pagination }) => {
- mock.onGet(TEST_URL).reply(200, TEST_DATA, headers);
-
- await setupSelect2AndSearch();
-
- expect(resultsSpy).toHaveBeenCalledWith(
- { results: TEST_DATA, pagination },
- 1,
- expect.anything(),
- );
- },
- );
- });
-});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 9fbb3d0a660..7aab1013fc0 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -192,9 +192,10 @@ describe('init markdown', () => {
});
describe('Continuing markdown lists', () => {
- const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
+ let enterEvent;
beforeEach(() => {
+ enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true });
textArea.addEventListener('keydown', keypressNoteText);
textArea.addEventListener('compositionstart', compositionStartNoteText);
textArea.addEventListener('compositionend', compositionEndNoteText);
@@ -256,7 +257,7 @@ describe('init markdown', () => {
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
- `('adds correct list continuation characters', ({ text, expected }) => {
+ `('remove list continuation characters', ({ text, expected }) => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
@@ -300,6 +301,37 @@ describe('init markdown', () => {
},
);
+ // test that when pressing Enter in the prefix area of a list item,
+ // such as between `2.`, we simply propagate the Enter,
+ // adding a newline. Since the event doesn't actually get propagated
+ // in the test, check that `defaultPrevented` is false
+ it.each`
+ text | add_at | prevented
+ ${'- one\n- two\n- three'} | ${6} | ${false}
+ ${'- one\n- two\n- three'} | ${7} | ${false}
+ ${'- one\n- two\n- three'} | ${8} | ${true}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${10} | ${false}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${15} | ${false}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${16} | ${true}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${10} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${11} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${17} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${18} | ${true}
+ ${'1. one\n2. two\n3. three'} | ${7} | ${false}
+ ${'1. one\n2. two\n3. three'} | ${9} | ${false}
+ ${'1. one\n2. two\n3. three'} | ${10} | ${true}
+ `(
+ 'allows a newline to be added if cursor is inside the list marker prefix area',
+ ({ text, add_at, prevented }) => {
+ textArea.value = text;
+ textArea.setSelectionRange(add_at, add_at);
+
+ textArea.dispatchEvent(enterEvent);
+
+ expect(enterEvent.defaultPrevented).toBe(prevented);
+ },
+ );
+
it('does not duplicate a line item for IME characters', () => {
const text = '- 日本語';
const expected = '- 日本語\n- ';
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index 0816152f4e3..39e0332631b 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -96,8 +96,8 @@ describe('initListbox', () => {
});
});
- it('passes the "right" prop through to the underlying component', () => {
- expect(listbox().props('right')).toBe(parsedAttributes.right);
+ it('passes the "placement" prop through to the underlying component', () => {
+ expect(listbox().props('placement')).toBe(parsedAttributes.placement);
});
});
});
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index 402a5e9db27..95db30a3683 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -3,15 +3,15 @@ import AccessRequestActionButtons from '~/members/components/action_buttons/acce
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
-import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import MemberActions from '~/members/components/table/member_actions.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
-describe('MemberActionButtons', () => {
+describe('MemberActions', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(MemberActionButtons, {
+ wrapper = shallowMount(MemberActions, {
propsData: {
isCurrentUser: false,
isInvitedUser: false,
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 1d18026a410..b8e0d73d8f6 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -5,7 +5,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
-import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import MemberActions from '~/members/components/table/member_actions.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MemberActivity from '~/members/components/table/member_activity.vue';
@@ -71,7 +71,7 @@ describe('MembersTable', () => {
'member-avatar',
'member-source',
'created-at',
- 'member-action-buttons',
+ 'member-actions',
'role-dropdown',
'remove-group-link-modal',
'remove-member-modal',
@@ -181,10 +181,7 @@ describe('MembersTable', () => {
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
expect(
- wrapper
- .find(`[data-label="Actions"][role="cell"]`)
- .findComponent(MemberActionButtons)
- .exists(),
+ wrapper.find(`[data-label="Actions"][role="cell"]`).findComponent(MemberActions).exists(),
).toBe(true);
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 9f200324c02..4f276e8c9df 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -166,9 +166,9 @@ describe('Members Utils', () => {
describe('canDisableTwoFactor', () => {
it.each`
- member | expected
- ${{ ...memberMock, canGetTwoFactorDisabled: true }} | ${false}
- ${{ ...memberMock, canGetTwoFactorDisabled: false }} | ${false}
+ member | expected
+ ${{ ...memberMock, canDisableTwoFactor: true }} | ${false}
+ ${{ ...memberMock, canDisableTwoFactor: false }} | ${false}
`(
'returns $expected for members whose two factor authentication can be disabled',
({ member, expected }) => {
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 50eac982e20..19ef4b7db25 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
@@ -35,7 +36,7 @@ describe('merge conflicts actions', () => {
const conflictsPath = 'conflicts/path/mock';
it('on success dispatches setConflictsData', () => {
- mock.onGet(conflictsPath).reply(200, {});
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_OK, {});
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -49,7 +50,7 @@ describe('merge conflicts actions', () => {
});
it('when data has type equal to error', () => {
- mock.onGet(conflictsPath).reply(200, { type: 'error', message: 'error message' });
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_OK, { type: 'error', message: 'error message' });
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -64,7 +65,7 @@ describe('merge conflicts actions', () => {
});
it('when request fails', () => {
- mock.onGet(conflictsPath).reply(400);
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -102,7 +103,7 @@ describe('merge conflicts actions', () => {
const resolveConflictsPath = 'resolve/conflicts/path/mock';
it('on success reloads the page', async () => {
- mock.onPost(resolveConflictsPath).reply(200, { redirect_to: 'hrefPath' });
+ mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_OK, { redirect_to: 'hrefPath' });
await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
@@ -114,7 +115,7 @@ describe('merge conflicts actions', () => {
});
it('on errors shows flash', async () => {
- mock.onPost(resolveConflictsPath).reply(400);
+ mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 16e3e49a297..579cee8c022 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -5,6 +5,7 @@ import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MergeRequest from '~/merge_request';
jest.mock('~/flash');
@@ -22,7 +23,7 @@ describe('MergeRequest', () => {
mock
.onPatch(`${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`)
- .reply(200, {});
+ .reply(HTTP_STATUS_OK, {});
test.merge = new MergeRequest();
return test.merge;
@@ -89,7 +90,7 @@ describe('MergeRequest', () => {
it('shows an error notification when tasklist update failed', async () => {
mock
.onPatch(`${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`)
- .reply(409, {});
+ .reply(HTTP_STATUS_CONFLICT, {});
$('.js-task-list-field').trigger({
type: 'tasklist:changed',
diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js
new file mode 100644
index 00000000000..8f84341b653
--- /dev/null
+++ b/spec/frontend/merge_requests/components/compare_app_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+import CompareApp from '~/merge_requests/components/compare_app.vue';
+
+let wrapper;
+
+function factory(provideData = {}) {
+ wrapper = shallowMount(CompareApp, {
+ provide: {
+ inputs: {
+ project: {
+ id: 'project',
+ name: 'project',
+ },
+ branch: {
+ id: 'branch',
+ name: 'branch',
+ },
+ },
+ toggleClass: {
+ project: 'project',
+ branch: 'branch',
+ },
+ i18n: {
+ projectHeaderText: 'Project',
+ branchHeaderText: 'Branch',
+ },
+ ...provideData,
+ },
+ });
+}
+
+describe('Merge requests compare app component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows commit box when selected branch is empty', () => {
+ factory({
+ currentBranch: {
+ text: '',
+ value: '',
+ },
+ });
+
+ const commitBox = wrapper.find('[data-testid="commit-box"]');
+
+ expect(commitBox.exists()).toBe(true);
+ expect(commitBox.text()).toBe('Select a branch to compare');
+ });
+});
diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
index 3fddbe7ae21..ab5c315816c 100644
--- a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
@@ -3,26 +3,32 @@ 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';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import CompareDropdown from '~/merge_requests/components/compare_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' },
+function factory(propsData = {}) {
+ wrapper = mount(CompareDropdown, {
+ propsData: {
+ endpoint: '/gitlab-org/gitlab/target_projects',
+ default: { value: 1, text: 'gitlab-org/gitlab' },
+ dropdownHeader: 'Select',
+ inputId: 'input_id',
+ inputName: 'input_name',
+ isProject: true,
+ ...propsData,
},
});
}
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
-describe('Merge requests target project dropdown component', () => {
+describe('Merge requests compare dropdown component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [
+ mock.onGet('/gitlab-org/gitlab/target_projects').reply(HTTP_STATUS_OK, [
{
id: 10,
name: 'Gitlab Test',
@@ -77,4 +83,22 @@ describe('Merge requests target project dropdown component', () => {
expect(mock.history.get[1].params).toEqual({ search: 'test' });
});
+
+ it('renders static data', async () => {
+ factory({
+ endpoint: undefined,
+ staticData: [
+ {
+ value: '10',
+ text: 'GitLab Org',
+ },
+ ],
+ });
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('li').length).toBe(1);
+ });
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index 6692a3b9347..87235fa843a 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
import eventHub from '~/milestones/event_hub';
+import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { createAlert } from '~/flash';
@@ -71,9 +72,9 @@ describe('Delete milestone modal', () => {
});
it.each`
- statusCode | alertMessage
- ${418} | ${`Failed to delete milestone ${mockProps.milestoneTitle}`}
- ${404} | ${`Milestone ${mockProps.milestoneTitle} was not found`}
+ statusCode | alertMessage
+ ${HTTP_STATUS_IM_A_TEAPOT} | ${`Failed to delete milestone ${mockProps.milestoneTitle}`}
+ ${HTTP_STATUS_NOT_FOUND} | ${`Milestone ${mockProps.milestoneTitle} was not found`}
`(
'displays error if deleting milestone failed with code $statusCode',
async ({ statusCode, alertMessage }) => {
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index c20c51db75e..f8ddca1a2ad 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -4,6 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import createStore from '~/milestones/stores/';
@@ -63,15 +64,15 @@ describe('Milestone combobox component', () => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
searchApiCallSpy = jest
.fn()
- .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
mock
.onGet(`/api/v4/projects/${projectId}/milestones`)
@@ -247,9 +248,11 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
- groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -300,7 +303,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -314,8 +317,10 @@ describe('Milestone combobox component', () => {
describe('when the project milestones search returns an error', () => {
beforeEach(() => {
- projectMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
- searchApiCallSpy = jest.fn().mockReturnValue([500]);
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
+ searchApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent({ value: [] });
@@ -363,7 +368,7 @@ describe('Milestone combobox component', () => {
createComponent();
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
return waitForRequests();
});
@@ -427,7 +432,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -441,8 +446,10 @@ describe('Milestone combobox component', () => {
describe('when the group milestones search returns an error', () => {
beforeEach(() => {
- groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
- searchApiCallSpy = jest.fn().mockReturnValue([500]);
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
+ searchApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent({ value: [] });
@@ -490,7 +497,11 @@ describe('Milestone combobox component', () => {
createComponent();
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ .mockReturnValue([
+ HTTP_STATUS_OK,
+ [{ title: 'group-v1.0' }],
+ { [X_TOTAL_HEADER]: '1' },
+ ]);
return waitForRequests();
});
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index 60657fbc9b8..d7ad3d29d0a 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -5,6 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
@@ -94,7 +95,7 @@ describe('Promote milestone modal', () => {
it('displays an error if promoting a milestone failed', async () => {
const dummyError = new Error('promoting milestone failed');
- dummyError.response = { status: 500 };
+ dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
return Promise.reject(dummyError);
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
index 0c3d3e78038..7d7eee2bc2c 100644
--- 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
@@ -24,14 +24,14 @@ exports[`MlCandidate renders correctly 1`] = `
<h2
class="gl-alert-title"
>
- Machine Learning Experiment Tracking is in Incubating Phase
+ 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
+ GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.
<a
class="gl-link"
@@ -39,7 +39,7 @@ exports[`MlCandidate renders correctly 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
+ Learn more about incubating features
</a>
</div>
@@ -58,7 +58,7 @@ exports[`MlCandidate renders correctly 1`] = `
class="gl-button-text"
>
- Feedback
+ Give feedback on this feature
</span>
</a>
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
deleted file mode 100644
index 3ee2c1cc075..00000000000
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
+++ /dev/null
@@ -1,761 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MlExperiment with candidates 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>
-
- Experiment candidates
-
- </h3>
-
- <table
- aria-busy="false"
- aria-colcount="9"
- class="table b-table gl-table gl-mt-0! ml-candidate-table table-sm"
- role="table"
- >
- <!---->
- <!---->
- <thead
- class=""
- role="rowgroup"
- >
- <!---->
- <tr
- class=""
- role="row"
- >
- <th
- aria-colindex="1"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Name
- </div>
- </th>
- <th
- aria-colindex="2"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Created at
- </div>
- </th>
- <th
- aria-colindex="3"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- User
- </div>
- </th>
- <th
- aria-colindex="4"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- L1 Ratio
- </div>
- </th>
- <th
- aria-colindex="5"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Rmse
- </div>
- </th>
- <th
- aria-colindex="6"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Auc
- </div>
- </th>
- <th
- aria-colindex="7"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Mae
- </div>
- </th>
- <th
- aria-colindex="8"
- aria-label="Details"
- class=""
- role="columnheader"
- scope="col"
- >
- <div />
- </th>
- <th
- aria-colindex="9"
- aria-label="Artifact"
- class=""
- role="columnheader"
- scope="col"
- >
- <div />
- </th>
- </tr>
- </thead>
- <tbody
- role="rowgroup"
- >
- <!---->
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title="aCandidate"
- >
- aCandidate
- </div>
- </td>
- <td
- aria-colindex="2"
- class=""
- role="cell"
- >
- <time
- class=""
- datetime="2023-01-05T14:07:02.975Z"
- title="2023-01-05T14:07:02.975Z"
- >
- in 2 years
- </time>
- </td>
- <td
- aria-colindex="3"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="/root"
- title="root"
- >
- @root
- </a>
- </td>
- <td
- aria-colindex="4"
- class=""
- role="cell"
- >
- <div
- title="0.4"
- >
- 0.4
- </div>
- </td>
- <td
- aria-colindex="5"
- class=""
- role="cell"
- >
- <div
- title="1"
- >
- 1
- </div>
- </td>
- <td
- aria-colindex="6"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="7"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="8"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_candidate1"
- title="Details"
- >
- Details
- </a>
- </td>
- <td
- aria-colindex="9"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_artifact"
- rel="noopener"
- target="_blank"
- title="Artifacts"
- >
- Artifacts
- </a>
- </td>
- </tr>
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="2"
- class=""
- role="cell"
- >
- <time
- class=""
- datetime="2023-01-05T14:07:02.975Z"
- title="2023-01-05T14:07:02.975Z"
- >
- in 2 years
- </time>
- </td>
- <td
- aria-colindex="3"
- class=""
- role="cell"
- >
- <div>
- -
- </div>
- </td>
- <td
- aria-colindex="4"
- class=""
- role="cell"
- >
- <div
- title="0.5"
- >
- 0.5
- </div>
- </td>
- <td
- aria-colindex="5"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="6"
- class=""
- role="cell"
- >
- <div
- title="0.3"
- >
- 0.3
- </div>
- </td>
- <td
- aria-colindex="7"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="8"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_candidate2"
- title="Details"
- >
- Details
- </a>
- </td>
- <td
- aria-colindex="9"
- class=""
- role="cell"
- >
- <div
- title="Artifacts"
- >
-
- -
-
- </div>
- </td>
- </tr>
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="2"
- class=""
- role="cell"
- >
- <time
- class=""
- datetime="2023-01-05T14:07:02.975Z"
- title="2023-01-05T14:07:02.975Z"
- >
- in 2 years
- </time>
- </td>
- <td
- aria-colindex="3"
- class=""
- role="cell"
- >
- <div>
- -
- </div>
- </td>
- <td
- aria-colindex="4"
- class=""
- role="cell"
- >
- <div
- title="0.5"
- >
- 0.5
- </div>
- </td>
- <td
- aria-colindex="5"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="6"
- class=""
- role="cell"
- >
- <div
- title="0.3"
- >
- 0.3
- </div>
- </td>
- <td
- aria-colindex="7"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="8"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_candidate3"
- title="Details"
- >
- Details
- </a>
- </td>
- <td
- aria-colindex="9"
- class=""
- role="cell"
- >
- <div
- title="Artifacts"
- >
-
- -
-
- </div>
- </td>
- </tr>
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="2"
- class=""
- role="cell"
- >
- <time
- class=""
- datetime="2023-01-05T14:07:02.975Z"
- title="2023-01-05T14:07:02.975Z"
- >
- in 2 years
- </time>
- </td>
- <td
- aria-colindex="3"
- class=""
- role="cell"
- >
- <div>
- -
- </div>
- </td>
- <td
- aria-colindex="4"
- class=""
- role="cell"
- >
- <div
- title="0.5"
- >
- 0.5
- </div>
- </td>
- <td
- aria-colindex="5"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="6"
- class=""
- role="cell"
- >
- <div
- title="0.3"
- >
- 0.3
- </div>
- </td>
- <td
- aria-colindex="7"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="8"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_candidate4"
- title="Details"
- >
- Details
- </a>
- </td>
- <td
- aria-colindex="9"
- class=""
- role="cell"
- >
- <div
- title="Artifacts"
- >
-
- -
-
- </div>
- </td>
- </tr>
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="2"
- class=""
- role="cell"
- >
- <time
- class=""
- datetime="2023-01-05T14:07:02.975Z"
- title="2023-01-05T14:07:02.975Z"
- >
- in 2 years
- </time>
- </td>
- <td
- aria-colindex="3"
- class=""
- role="cell"
- >
- <div>
- -
- </div>
- </td>
- <td
- aria-colindex="4"
- class=""
- role="cell"
- >
- <div
- title="0.5"
- >
- 0.5
- </div>
- </td>
- <td
- aria-colindex="5"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="6"
- class=""
- role="cell"
- >
- <div
- title="0.3"
- >
- 0.3
- </div>
- </td>
- <td
- aria-colindex="7"
- class=""
- role="cell"
- >
- <div
- title=""
- >
-
- </div>
- </td>
- <td
- aria-colindex="8"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_candidate5"
- title="Details"
- >
- Details
- </a>
- </td>
- <td
- aria-colindex="9"
- class=""
- role="cell"
- >
- <div
- title="Artifacts"
- >
-
- -
-
- </div>
- </td>
- </tr>
- <!---->
- <!---->
- </tbody>
- <!---->
- </table>
-
- <!---->
-</div>
-`;
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
index fb45c4b07a4..483e454d7d7 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
@@ -28,7 +28,7 @@ describe('MlCandidate', () => {
},
};
- return mountExtended(MlCandidate, { provide: { candidate } });
+ return mountExtended(MlCandidate, { propsData: { candidate } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
index abcaf17303f..f307d2c5a58 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,140 +1,315 @@
-import { GlAlert, GlPagination } from '@gitlab/ui';
+import { GlAlert, GlTable, GlLink } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as urlHelpers from '~/lib/utils/url_utility';
describe('MlExperiment', () => {
let wrapper;
+ const startCursor = 'eyJpZCI6IjE2In0';
+ const defaultPageInfo = {
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
const createWrapper = (
candidates = [],
metricNames = [],
paramNames = [],
- pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ pageInfo = defaultPageInfo,
) => {
- return mountExtended(MlExperiment, {
- provide: { candidates, metricNames, paramNames, pagination },
+ wrapper = mountExtended(MlExperiment, {
+ provide: { candidates, metricNames, paramNames, pageInfo },
});
};
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const candidates = [
+ {
+ rmse: 1,
+ l1_ratio: 0.4,
+ details: 'link_to_candidate1',
+ artifact: 'link_to_artifact',
+ name: 'aCandidate',
+ created_at: '2023-01-05T14:07:02.975Z',
+ user: { username: 'root', path: '/root' },
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate2',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate3',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate4',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate5',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ ];
+
+ const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => {
+ createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
+ };
- const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findPagination = () => wrapper.findComponent(Pagination);
+ const findEmptyState = () => wrapper.findByText('No candidates to display');
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableHeaders = () => findTable().findAll('th');
+ const findTableRows = () => findTable().findAll('tbody > tr');
+ const findNthTableRow = (idx) => findTableRows().at(idx);
+ const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+ const hrefInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).attributes().href;
it('shows incubation warning', () => {
- wrapper = createWrapper();
+ createWrapper();
expect(findAlert().exists()).toBe(true);
});
- describe('no candidates', () => {
- it('shows empty state', () => {
- wrapper = createWrapper();
+ describe('default inputs', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await nextTick();
+ });
+
+ it('shows empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('does not show pagination', () => {
- wrapper = createWrapper();
+ expect(findPagination().exists()).toBe(false);
+ });
- expect(wrapper.findComponent(GlPagination).exists()).toBe(false);
+ it('there are no columns', () => {
+ expect(findTable().findAll('th')).toHaveLength(0);
+ });
+
+ it('initializes sorting correctly', () => {
+ expect(findRegistrySearch().props('sorting')).toMatchObject({
+ orderBy: 'created_at',
+ sort: 'desc',
+ });
+ });
+
+ it('initializes filters correctly', () => {
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: '' } }]);
});
});
- describe('with candidates', () => {
- const defaultPagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 5 };
-
- const createWrapperWithCandidates = (pagination = defaultPagination) => {
- return createWrapper(
- [
- {
- rmse: 1,
- l1_ratio: 0.4,
- details: 'link_to_candidate1',
- artifact: 'link_to_artifact',
- name: 'aCandidate',
- created_at: '2023-01-05T14:07:02.975Z',
- user: { username: 'root', path: '/root' },
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate2',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate3',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate4',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate5',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- ],
- ['rmse', 'auc', 'mae'],
- ['l1_ratio'],
- pagination,
- );
- };
-
- it('renders correctly', () => {
- wrapper = createWrapperWithCandidates();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('Pagination behaviour', () => {
- it('should show', () => {
- wrapper = createWrapperWithCandidates();
-
- expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
+ describe('Search', () => {
+ it('shows search box', () => {
+ createWrapper();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ });
+
+ it('metrics are added as options for sorting', () => {
+ createWrapper([], ['bar']);
+
+ const labels = findRegistrySearch()
+ .props('sortableFields')
+ .map((e) => e.orderBy);
+ expect(labels).toContain('metric.bar');
+ });
+
+ it('sets the component filters based on the querystring', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: 'A' } }]);
+ });
+
+ it('sets the component sort based on the querystring', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy: 'B', sort: 'c' });
+ });
+
+ it('sets the component sort based on the querystring, when order by is a metric', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C&orderByType=metric');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({
+ orderBy: 'metric.B',
+ sort: 'c',
+ });
+ });
+
+ describe('Search submit', () => {
+ beforeEach(() => {
+ setWindowLocation('https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc');
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ createWrapper();
+ });
+
+ it('On submit, resets the cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc',
+ );
});
- it('should get the page number from the URL', () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+ it('On sorting changed, resets cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'created_at' });
- expect(wrapper.findComponent(GlPagination).props().value).toBe(2);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=created_at&orderByType=column&sort=asc',
+ );
});
- it('should not have a prevPage if the page is 1', () => {
- wrapper = createWrapperWithCandidates();
+ it('On sorting changed and is metric, resets cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'metric.auc' });
- expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(null);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=auc&orderByType=metric&sort=asc',
+ );
});
- it('should set the prevPage to 1 if the page is 2', () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+ it('On direction changed, reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { sort: 'desc' });
- expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=desc',
+ );
});
+ });
+ });
+
+ describe('Pagination behaviour', () => {
+ beforeEach(() => {
+ createWrapperWithCandidates();
+ });
+
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
- it('should not have a nextPage if isLastPage is true', async () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
+ it('Passes pagination to pagination component', () => {
+ createWrapperWithCandidates();
+
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
+ });
+
+ describe('Candidate table', () => {
+ const firstCandidateIndex = 0;
+ const secondCandidateIndex = 1;
+ const firstCandidate = candidates[firstCandidateIndex];
+
+ beforeEach(() => {
+ createWrapperWithCandidates();
+ });
+
+ it('renders all rows', () => {
+ expect(findTableRows()).toHaveLength(candidates.length);
+ });
+
+ it('sets the correct columns in the table', () => {
+ const expectedColumnNames = [
+ 'Name',
+ 'Created at',
+ 'User',
+ 'L1 Ratio',
+ 'Rmse',
+ 'Auc',
+ 'Mae',
+ '',
+ '',
+ ];
+
+ expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
+ });
- expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(null);
+ describe('Artifact column', () => {
+ const artifactColumnIndex = -1;
+
+ it('shows the a link to the artifact', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, artifactColumnIndex)).toBe(
+ firstCandidate.artifact,
+ );
+ });
+
+ it('shows empty state when no artifact', () => {
+ expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-');
+ });
+ });
+
+ describe('User column', () => {
+ const userColumn = 2;
+
+ it('creates a link to the user', () => {
+ const column = findColumnInRow(firstCandidateIndex, userColumn).findComponent(GlLink);
+
+ expect(column.attributes().href).toBe(firstCandidate.user.path);
+ expect(column.text()).toBe(`@${firstCandidate.user.username}`);
+ });
+
+ it('when there is no user shows empty state', () => {
+ createWrapperWithCandidates();
+
+ expect(findColumnInRow(secondCandidateIndex, userColumn).text()).toBe('-');
});
+ });
+
+ describe('Candidate name column', () => {
+ const nameColumnIndex = 0;
+
+ it('Sets the name', () => {
+ expect(findColumnInRow(firstCandidateIndex, nameColumnIndex).text()).toBe(
+ firstCandidate.name,
+ );
+ });
+
+ it('when there is no user shows nothing', () => {
+ expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('');
+ });
+ });
- it('should set the nextPage to 2 if the page is 1', () => {
- wrapper = createWrapperWithCandidates();
+ describe('Detail column', () => {
+ const detailColumn = -2;
- expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(2);
+ it('is a link to details', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details);
});
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
new file mode 100644
index 00000000000..017db647ac6
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
@@ -0,0 +1,110 @@
+import { GlEmptyState, GlLink, GlTableLite } from '@gitlab/ui';
+import MlExperimentsIndexApp from '~/ml/experiment_tracking/routes/experiments/index';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ startCursor,
+ firstExperiment,
+ secondExperiment,
+ experiments,
+ defaultPageInfo,
+} from './mock_data';
+
+let wrapper;
+const createWrapper = (defaultExperiments = [], pageInfo = defaultPageInfo) => {
+ wrapper = mountExtended(MlExperimentsIndexApp, {
+ propsData: { experiments: defaultExperiments, pageInfo },
+ });
+};
+
+const findAlert = () => wrapper.findComponent(IncubationAlert);
+const findPagination = () => wrapper.findComponent(Pagination);
+const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+const findTable = () => wrapper.findComponent(GlTableLite);
+const findTableHeaders = () => findTable().findAll('th');
+const findTableRows = () => findTable().findAll('tbody > tr');
+const findNthTableRow = (idx) => findTableRows().at(idx);
+const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+const hrefInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).attributes().href;
+
+describe('MlExperimentsIndex', () => {
+ describe('empty state', () => {
+ beforeEach(() => createWrapper());
+
+ it('displays empty state when no experiment', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('does not show table', () => {
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ it('displays IncubationAlert', () => {
+ createWrapper(experiments);
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('experiments table', () => {
+ const firstRow = 0;
+ const secondRow = 1;
+ const nameColumn = 0;
+ const candidateCountColumn = 1;
+
+ beforeEach(() => createWrapper(experiments));
+
+ it('displays the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('sets headers correctly', () => {
+ const expectedColumnNames = ['Experiment', 'Logged candidates for experiment'];
+
+ expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
+ });
+
+ describe('experiment name column', () => {
+ it('displays the experiment name', () => {
+ expect(findColumnInRow(firstRow, nameColumn).text()).toBe(firstExperiment.name);
+ expect(findColumnInRow(secondRow, nameColumn).text()).toBe(secondExperiment.name);
+ });
+
+ it('is a link to the experiment', () => {
+ expect(hrefInRowAndColumn(firstRow, nameColumn)).toBe(firstExperiment.path);
+ expect(hrefInRowAndColumn(secondRow, nameColumn)).toBe(secondExperiment.path);
+ });
+ });
+
+ describe('candidate count column', () => {
+ it('shows the candidate count', () => {
+ expect(findColumnInRow(firstRow, candidateCountColumn).text()).toBe(
+ `${firstExperiment.candidate_count}`,
+ );
+ expect(findColumnInRow(secondRow, candidateCountColumn).text()).toBe(
+ `${secondExperiment.candidate_count}`,
+ );
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ describe('Pagination behaviour', () => {
+ beforeEach(() => createWrapper(experiments));
+
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('Passes pagination to pagination component', () => {
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js
new file mode 100644
index 00000000000..ea02584a7cc
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js
@@ -0,0 +1,21 @@
+export const startCursor = 'eyJpZCI6IjE2In0';
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
+
+export const firstExperiment = Object.freeze({
+ name: 'Experiment 1',
+ path: 'path/to/experiment/1',
+ candidate_count: 2,
+});
+
+export const secondExperiment = Object.freeze({
+ name: 'Experiment 2',
+ path: 'path/to/experiment/2',
+ candidate_count: 3,
+});
+
+export const experiments = [firstExperiment, secondExperiment];
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
index 08487a7a796..4483c9fd39f 100644
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
@@ -5,6 +5,7 @@ exports[`EmptyState shows gettingStarted state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
invertindarkmode="true"
primarybuttonlink="/clustersPath"
@@ -22,6 +23,7 @@ exports[`EmptyState shows noData state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="You are connected to the Prometheus server, but there is currently no data to display."
invertindarkmode="true"
primarybuttonlink="/settingsPath"
@@ -39,6 +41,7 @@ exports[`EmptyState shows unableToConnect state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="Ensure connectivity is available from the GitLab server to the Prometheus server"
invertindarkmode="true"
primarybuttonlink="/documentationPath"
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
index 1d7ff420a17..42a16a39dfd 100644
--- a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
@@ -3,6 +3,7 @@
exports[`GroupEmptyState given state BAD_QUERY passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
@@ -31,6 +32,7 @@ exports[`GroupEmptyState given state BAD_QUERY renders the slotted content 1`] =
exports[`GroupEmptyState given state CONNECTION_FAILED passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating.",
"invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
@@ -48,6 +50,7 @@ exports[`GroupEmptyState given state CONNECTION_FAILED renders the slotted conte
exports[`GroupEmptyState given state FOO STATE passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "An error occurred while loading the data. Please try again.",
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -65,6 +68,7 @@ exports[`GroupEmptyState given state FOO STATE renders the slotted content 1`] =
exports[`GroupEmptyState given state LOADING passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.",
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -82,6 +86,7 @@ exports[`GroupEmptyState given state LOADING renders the slotted content 1`] = `
exports[`GroupEmptyState given state NO_DATA passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -110,6 +115,7 @@ exports[`GroupEmptyState given state NO_DATA renders the slotted content 1`] = `
exports[`GroupEmptyState given state TIMEOUT passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -138,6 +144,7 @@ exports[`GroupEmptyState given state TIMEOUT renders the slotted content 1`] = `
exports[`GroupEmptyState given state UNKNOWN_ERROR passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "An error occurred while loading the data. Please try again.",
"invertInDarkMode": true,
"primaryButtonLink": null,
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index cf7df3dd9d5..308895768a4 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_OK,
HTTP_STATUS_SERVICE_UNAVAILABLE,
@@ -55,7 +56,7 @@ describe('monitoring metrics_requests', () => {
});
it('rejects after getting an error', () => {
- mock.onGet(dashboardEndpoint).reply(500);
+ mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return getDashboard(dashboardEndpoint, params).catch((error) => {
expect(error).toEqual(expect.any(Error));
@@ -99,7 +100,7 @@ describe('monitoring metrics_requests', () => {
});
it('rejects after getting an HTTP 500 error', () => {
- mock.onGet(prometheusEndpoint).reply(500, {
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
status: 'error',
error: 'An error occurred',
});
@@ -125,7 +126,7 @@ describe('monitoring metrics_requests', () => {
// Mock multiple attempts while the cache is filling up and fails
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(500, {
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
status: 'error',
error: 'An error occurred',
}); // 3rd attempt
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index fbe030b1a7d..8eda46a2ff1 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -7,6 +7,7 @@ import * as commonUtils from '~/lib/utils/common_utils';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_CREATED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_OK,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
@@ -205,7 +206,7 @@ describe('Monitoring store actions', () => {
it('on success, dispatches receive and success actions, then fetches dashboard warnings', () => {
document.body.dataset.page = 'projects:environments:metrics';
- mock.onGet(state.dashboardEndpoint).reply(200, response);
+ mock.onGet(state.dashboardEndpoint).reply(HTTP_STATUS_OK, response);
return testAction(
fetchDashboard,
@@ -231,7 +232,9 @@ describe('Monitoring store actions', () => {
fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
};
result = () => {
- mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
+ mock
+ .onGet(state.dashboardEndpoint)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, mockDashboardsErrorResponse);
return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
};
});
@@ -417,7 +420,7 @@ describe('Monitoring store actions', () => {
});
it('commits result', () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
return testAction(
fetchPrometheusMetric,
@@ -450,7 +453,7 @@ describe('Monitoring store actions', () => {
};
it('uses calculated step', async () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
await testAction(
fetchPrometheusMetric,
@@ -489,7 +492,7 @@ describe('Monitoring store actions', () => {
};
it('uses metric step', async () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
await testAction(
fetchPrometheusMetric,
@@ -517,7 +520,7 @@ describe('Monitoring store actions', () => {
});
it('commits failure, when waiting for results and getting a server error', async () => {
- mock.onGet(prometheusEndpointPath).reply(500);
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const error = new Error('Request failed with status code 500');
@@ -552,7 +555,7 @@ describe('Monitoring store actions', () => {
describe('fetchDeploymentsData', () => {
it('dispatches receiveDeploymentsDataSuccess on success', () => {
state.deploymentsEndpoint = '/success';
- mock.onGet(state.deploymentsEndpoint).reply(200, {
+ mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_OK, {
deployments: deploymentData,
});
@@ -566,7 +569,7 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveDeploymentsDataFailure on error', () => {
state.deploymentsEndpoint = '/error';
- mock.onGet(state.deploymentsEndpoint).reply(500);
+ mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
fetchDeploymentsData,
@@ -918,7 +921,7 @@ describe('Monitoring store actions', () => {
it('stars dashboard if it is not starred', () => {
state.selectedDashboard = unstarredDashboard;
- mock.onPost(unstarredDashboard.user_starred_path).reply(200);
+ mock.onPost(unstarredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
@@ -934,7 +937,7 @@ describe('Monitoring store actions', () => {
it('unstars dashboard if it is starred', () => {
state.selectedDashboard = starredDashboard;
- mock.onPost(starredDashboard.user_starred_path).reply(200);
+ mock.onPost(starredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
@@ -1065,7 +1068,7 @@ describe('Monitoring store actions', () => {
},
];
- mock.onGet('/series?match[]=metric_name').reply(200, {
+ mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_OK, {
status: 'success',
data,
});
@@ -1085,7 +1088,7 @@ describe('Monitoring store actions', () => {
});
it('should notify the user that dynamic options were not loaded', () => {
- mock.onGet('/series?match[]=metric_name').reply(500);
+ mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
@@ -1150,7 +1153,9 @@ describe('Monitoring store actions', () => {
});
it('should display a generic error when the backend fails', () => {
- mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
+ mock
+ .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
diff --git a/spec/frontend/mr_notes/stores/actions_spec.js b/spec/frontend/mr_notes/stores/actions_spec.js
index 568c1b930c9..ae30ed1f0b3 100644
--- a/spec/frontend/mr_notes/stores/actions_spec.js
+++ b/spec/frontend/mr_notes/stores/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createStore } from '~/mr_notes/stores';
describe('MR Notes Mutator Actions', () => {
@@ -31,7 +31,7 @@ describe('MR Notes Mutator Actions', () => {
mock = new MockAdapter(axios);
- mock.onGet(metadata).reply(200, mrMetadata);
+ mock.onGet(metadata).reply(HTTP_STATUS_OK, mrMetadata);
});
afterEach(() => {
@@ -54,7 +54,7 @@ describe('MR Notes Mutator Actions', () => {
});
it('should set failedToLoadMetadata flag when request fails', async () => {
- mock.onGet(metadata).reply(500);
+ mock.onGet(metadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await store.dispatch('fetchMrMetadata');
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index ee75dfb70e4..bad24345f9d 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -3,6 +3,7 @@ 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -74,7 +75,7 @@ describe('NewNavToggle', () => {
});
it('reloads the page on success', async () => {
- mock.onPut(TEST_ENDPONT).reply(200);
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
actFn();
await waitForPromises();
@@ -83,7 +84,7 @@ describe('NewNavToggle', () => {
});
it('shows an alert on error', async () => {
- mock.onPut(TEST_ENDPONT).reply(500);
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
actFn();
await waitForPromises();
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index b32ab5ebe09..e70f70afc97 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -65,7 +65,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
label: 'hamburger_menu',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
index 0218f09af0a..293fe361fa9 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -103,6 +103,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
expect(findMenuSections().props()).toEqual({
sections,
withTopBorder: true,
+ isPrimarySection: false,
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 048fca846ad..8a0340087ec 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -56,6 +56,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
{ id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
],
withTopBorder: false,
+ isPrimarySection: true,
});
});
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
index 0ed5cffd93f..7a5a8475ab7 100644
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -80,7 +80,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
}),
},
{
- classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
+ classes: [
+ ...TopNavMenuSections.BORDER_CLASSES.split(' '),
+ 'gl-border-gray-50',
+ 'gl-mt-3',
+ ],
menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
@@ -117,8 +121,21 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
it('renders border classes for top section', () => {
expect(findSectionModels().map((x) => x.classes)).toEqual([
- [...TopNavMenuSections.BORDER_CLASSES.split(' ')],
- [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50'],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50', 'gl-mt-3'],
+ ]);
+ });
+ });
+
+ describe('with isPrimarySection=true', () => {
+ beforeEach(() => {
+ createComponent({ isPrimarySection: true });
+ });
+
+ it('renders border classes for top section', () => {
+ expect(findSectionModels().map((x) => x.classes)).toEqual([
+ [],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-100', 'gl-mt-3'],
]);
});
});
diff --git a/spec/frontend/notes/components/attachments_warning_spec.js b/spec/frontend/notes/components/attachments_warning_spec.js
new file mode 100644
index 00000000000..0e99c26ed2b
--- /dev/null
+++ b/spec/frontend/notes/components/attachments_warning_spec.js
@@ -0,0 +1,16 @@
+import { mount } from '@vue/test-utils';
+import AttachmentsWarning from '~/notes/components/attachments_warning.vue';
+
+describe('Attachments Warning Component', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(AttachmentsWarning);
+ });
+
+ it('shows warning', () => {
+ const expected =
+ 'Attachments are sent by email. Attachments over 10 MB are sent as links to your GitLab instance, and only accessible to project members.';
+ expect(wrapper.text()).toBe(expected);
+ });
+});
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index 6662492fd81..93b54f95021 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -1,17 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
+import AttachmentsWarning from '~/notes/components/attachments_warning.vue';
import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
describe('Comment Field Layout Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const LOCKED_DISCUSSION_DOCS_PATH = 'docs/locked/path';
const CONFIDENTIAL_ISSUES_DOCS_PATH = 'docs/confidential/path';
@@ -22,18 +18,32 @@ describe('Comment Field Layout Component', () => {
confidential_issues_docs_path: CONFIDENTIAL_ISSUES_DOCS_PATH,
};
+ const commentFieldWithAttachmentData = {
+ noteableData: {
+ ...noteableDataMock,
+ issue_email_participants: [{ email: 'someone@gitlab.com' }, { email: 'another@gitlab.com' }],
+ },
+ containsLink: true,
+ };
+
const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning);
const findEmailParticipantsWarning = () => wrapper.findComponent(EmailParticipantsWarning);
+ const findAttachmentsWarning = () => wrapper.findComponent(AttachmentsWarning);
const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
- const createWrapper = (props = {}, slots = {}) => {
+ const createWrapper = (props = {}, provide = {}) => {
wrapper = extendedWrapper(
shallowMount(CommentFieldLayout, {
propsData: {
noteableData: noteableDataMock,
...props,
},
- slots,
+ provide: {
+ glFeatures: {
+ serviceDeskNewNoteEmailNativeAttachments: true,
+ },
+ ...provide,
+ },
}),
);
};
@@ -108,23 +118,25 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
+
+ it('does not show AttachmentWarning', () => {
+ createWrapper();
+
+ expect(findAttachmentsWarning().exists()).toBe(false);
+ });
});
describe('issue has email participants', () => {
beforeEach(() => {
- createWrapper({
- noteableData: {
- ...noteableDataMock,
- issue_email_participants: [
- { email: 'someone@gitlab.com' },
- { email: 'another@gitlab.com' },
- ],
- },
- });
+ createWrapper(commentFieldWithAttachmentData);
});
it('shows EmailParticipantsWarning', () => {
- expect(findEmailParticipantsWarning().isVisible()).toBe(true);
+ expect(findEmailParticipantsWarning().exists()).toBe(true);
+ });
+
+ it('shows AttachmentsWarning', () => {
+ expect(findAttachmentsWarning().isVisible()).toBe(true);
});
it('sets EmailParticipantsWarning props', () => {
@@ -148,4 +160,22 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
});
+
+ describe('serviceDeskNewNoteEmailNativeAttachments flag', () => {
+ it('shows warning message when flag is enabled', () => {
+ createWrapper(commentFieldWithAttachmentData, {
+ glFeatures: { serviceDeskNewNoteEmailNativeAttachments: true },
+ });
+
+ expect(findAttachmentsWarning().exists()).toBe(true);
+ });
+
+ it('shows warning message when flag is disables', () => {
+ createWrapper(commentFieldWithAttachmentData, {
+ glFeatures: { serviceDeskNewNoteEmailNativeAttachments: false },
+ });
+
+ expect(findAttachmentsWarning().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index e13985ef469..dfb05c85fc8 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -10,6 +10,7 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import CommentForm from '~/notes/components/comment_form.vue';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
import * as constants from '~/notes/constants';
@@ -162,11 +163,11 @@ describe('issue_comment_form component', () => {
});
it.each`
- httpStatus | errors
- ${400} | ${[COMMENT_FORM.GENERIC_UNSUBMITTABLE_NETWORK]}
- ${422} | ${['error 1']}
- ${422} | ${['error 1', 'error 2']}
- ${422} | ${['error 1', 'error 2', 'error 3']}
+ httpStatus | errors
+ ${400} | ${[COMMENT_FORM.GENERIC_UNSUBMITTABLE_NETWORK]}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1']}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2']}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2', 'error 3']}
`(
'displays the correct errors ($errors) for a $httpStatus network response',
async ({ errors, httpStatus }) => {
@@ -198,7 +199,10 @@ describe('issue_comment_form component', () => {
store = createStore({
actions: {
saveNote: jest.fn().mockRejectedValue({
- response: { status: 422, data: { errors: { commands_only: [...commandErrors] } } },
+ response: {
+ status: HTTP_STATUS_UNPROCESSABLE_ENTITY,
+ data: { errors: { commands_only: [...commandErrors] } },
+ },
}),
},
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index ed9fc47540d..ed1ced1b3d1 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -7,6 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import createEventHub from '~/helpers/event_hub_factory';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import Tracking from '~/tracking';
@@ -74,7 +75,7 @@ describe('DiscussionFilter component', () => {
// We are mocking the discussions retrieval,
// as it doesn't matter for our tests here
- mock.onGet(DISCUSSION_PATH).reply(200, '');
+ mock.onGet(DISCUSSION_PATH).reply(HTTP_STATUS_OK, '');
window.mrTabs = undefined;
wrapper = mountComponent();
jest.spyOn(Tracking, 'event');
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index c7420ca9c48..8630b7b7d07 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -7,6 +7,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue';
import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import createStore from '~/notes/stores';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { userDataMock } from '../mock_data';
@@ -21,6 +22,7 @@ describe('noteActions', () => {
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
+ const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-button"]`);
const setupStoreForIncidentTimelineEvents = ({
userCanAdd,
@@ -63,7 +65,6 @@ describe('noteActions', () => {
noteId: '539',
noteUrl: `${TEST_HOST}/group/project/-/merge_requests/1#note_1`,
projectName: 'project',
- reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
showReply: false,
awardPath: `${TEST_HOST}/award_emoji`,
};
@@ -115,7 +116,7 @@ describe('noteActions', () => {
});
it('should be possible to report abuse to admin', () => {
- expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true);
+ expect(findReportAbuseButton().exists()).toBe(true);
});
it('should be possible to copy link to a note', () => {
@@ -373,4 +374,53 @@ describe('noteActions', () => {
});
});
});
+
+ describe('report abuse button', () => {
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ describe('when user is not allowed to report abuse', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', userDataMock);
+ wrapper = mountNoteActions({ ...props, canReportAsAbuse: false });
+ });
+
+ it('does not render the report abuse', () => {
+ expect(findReportAbuseButton().exists()).toBe(false);
+ });
+
+ it('does not render the abuse category drawer', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
+
+ describe('when user is allowed to report abuse', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', userDataMock);
+ wrapper = mountNoteActions({ ...props, canReportAsAbuse: true });
+ });
+
+ it('renders report abuse button', () => {
+ expect(findReportAbuseButton().exists()).toBe(true);
+ });
+
+ it('does not render the abuse category drawer immediately', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('opens the drawer when report abuse button is clicked', async () => {
+ await findReportAbuseButton().trigger('click');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
+ });
+
+ it('closes the drawer', async () => {
+ await findReportAbuseButton().trigger('click');
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 9fc89ffa473..89ac0216f41 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -2,6 +2,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import awardsNote from '~/notes/components/note_awards_list.vue';
import createStore from '~/notes/stores';
import { noteableDataMock, notesDataMock } from '../mock_data';
@@ -17,7 +18,7 @@ describe('note_awards_list component', () => {
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
- mock.onPost(toggleAwardPath).reply(200, '');
+ mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, '');
const Component = Vue.extend(awardsNote);
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 3d7195752d3..af1b4f64037 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -34,6 +34,9 @@ const singleLineNotePosition = {
describe('issue_note', () => {
let store;
let wrapper;
+
+ const REPORT_ABUSE_PATH = '/abuse_reports/add_category';
+
const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
const createWrapper = (props = {}, storeUpdater = (s) => s) => {
@@ -62,6 +65,9 @@ describe('issue_note', () => {
'note-body',
'multiline-comment-form',
],
+ provide: {
+ reportAbusePath: REPORT_ABUSE_PATH,
+ },
});
};
@@ -241,7 +247,6 @@ describe('issue_note', () => {
expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit);
expect(noteActionsProps.canReportAsAbuse).toBe(true);
expect(noteActionsProps.canResolve).toBe(false);
- expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path);
expect(noteActionsProps.resolvable).toBe(false);
expect(noteActionsProps.isResolved).toBe(false);
expect(noteActionsProps.isResolving).toBe(false);
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 0c3d0da4f0f..b08a22f8674 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -7,6 +7,7 @@ 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 { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getLocationHash } from '~/lib/utils/url_utility';
import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
@@ -286,7 +287,7 @@ describe('note_app', () => {
describe('emoji awards', () => {
beforeEach(() => {
- axiosMock.onAny().reply(200, []);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, []);
wrapper = mountComponent();
return waitForPromises();
});
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index f52c3e28691..6d3bc19bd45 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -7,6 +7,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
// These must be imported synchronously because they pull dependencies
@@ -27,6 +28,10 @@ window.gl = window.gl || {};
gl.utils = gl.utils || {};
gl.utils.disableButtonIfEmptyField = () => {};
+function wrappedDiscussionNote(note) {
+ return `<table><tbody>${note}</tbody></table>`;
+}
+
// the following test is unreliable and failing in main 2-3 times a day
// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
// eslint-disable-next-line jest/no-disabled-tests
@@ -75,7 +80,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
describe('task lists', () => {
beforeEach(() => {
- mockAxios.onAny().reply(200, {});
+ mockAxios.onAny().reply(HTTP_STATUS_OK, {});
new Notes('', []);
});
@@ -181,7 +186,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
const $form = $('form.js-main-target-form');
$form.find('textarea.js-note-text').val(sampleComment);
- mockAxios.onPost(NOTES_POST_PATH).reply(200, noteEntity);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, noteEntity);
});
it('updates note and resets edit form', () => {
@@ -435,22 +440,40 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
);
});
- it('should append to row selected with line_code', () => {
- $form.length = 0;
- note.discussion_line_code = 'line_code';
- note.diff_discussion_html = '<tr></tr>';
+ describe('HTML output', () => {
+ let line;
- const line = document.createElement('div');
- line.id = note.discussion_line_code;
- document.body.appendChild(line);
+ beforeEach(() => {
+ $form.length = 0;
+ note.discussion_line_code = 'line_code';
+ note.diff_discussion_html = '<tr></tr>';
- // Override mocks for this single test
- $form.closest.mockReset();
- $form.closest.mockReturnValue($form);
+ line = document.createElement('div');
+ line.id = note.discussion_line_code;
+ document.body.appendChild(line);
- Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+ // Override mocks for these tests
+ $form.closest.mockReset();
+ $form.closest.mockReturnValue($form);
+ });
- expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
+ it('should append to row selected with line_code', () => {
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(
+ wrappedDiscussionNote(note.diff_discussion_html),
+ );
+ });
+
+ it('sanitizes the output html without stripping leading <tr> or <td> elements', () => {
+ const sanitizedDiscussion = '<tr><td><a>I am a dolphin!</a></td></tr>';
+ note.diff_discussion_html =
+ '<tr><td><a href="javascript:alert(1)">I am a dolphin!</a></td></tr>';
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(wrappedDiscussionNote(sanitizedDiscussion));
+ });
});
});
@@ -546,7 +569,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
function mockNotesPost() {
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
}
function mockNotesPostError() {
@@ -591,7 +614,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
};
mockAxios.onPost(NOTES_POST_PATH).replyOnce(() => {
expect($submitButton).toBeDisabled();
- return [200, note];
+ return [HTTP_STATUS_OK, note];
});
await notes.postComment(dummyEvent);
@@ -650,7 +673,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
loadHTMLFixture('commit/show.html');
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
window.gon.current_username = 'root';
@@ -695,7 +718,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
note: sampleComment,
valid: true,
};
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
$form = $('form.js-main-target-form');
@@ -730,7 +753,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
beforeEach(() => {
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
window.gon.current_username = 'root';
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index 286f2adc1d8..d5b7ad73177 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -1,4 +1,5 @@
// Copied to ee/spec/frontend/notes/mock_data.js
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { __ } from '~/locale';
export const notesDataMock = {
@@ -655,11 +656,11 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
};
export function getIndividualNoteResponse(config) {
- return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+ return [HTTP_STATUS_OK, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export function getDiscussionNoteResponse(config) {
- return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+ return [HTTP_STATUS_OK, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export const notesWithDescriptionChanges = [
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 0b2623f3d77..c4c0dc58b0d 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -7,6 +7,11 @@ 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';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
+} from '~/lib/utils/http_status';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import * as actions from '~/notes/stores/actions';
@@ -175,7 +180,7 @@ describe('Actions Notes Store', () => {
describe('async methods', () => {
beforeEach(() => {
- axiosMock.onAny().reply(200, {});
+ axiosMock.onAny().reply(HTTP_STATUS_OK, {});
});
describe('closeMergeRequest', () => {
@@ -249,8 +254,9 @@ describe('Actions Notes Store', () => {
const pollResponse = { notes: [], last_fetched_at: '123456' };
const pollHeaders = { 'poll-interval': `${pollInterval}` };
const successMock = () =>
- axiosMock.onGet(notesDataMock.notesPath).reply(200, pollResponse, pollHeaders);
- const failureMock = () => axiosMock.onGet(notesDataMock.notesPath).reply(500);
+ axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, pollResponse, pollHeaders);
+ const failureMock = () =>
+ axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const advanceAndRAF = async (time) => {
if (time) {
jest.advanceTimersByTime(time);
@@ -343,11 +349,11 @@ describe('Actions Notes Store', () => {
axiosMock
.onGet(notesDataMock.notesPath)
- .replyOnce(500) // cause one error
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR) // cause one error
.onGet(notesDataMock.notesPath)
- .replyOnce(200, pollResponse, pollHeaders) // then a success
+ .replyOnce(HTTP_STATUS_OK, pollResponse, pollHeaders) // then a success
.onGet(notesDataMock.notesPath)
- .reply(500); // and then more errors
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); // and then more errors
await startPolling(); // Failure #1
await advanceXMoreIntervals(1); // Success #1
@@ -398,7 +404,7 @@ describe('Actions Notes Store', () => {
const endpoint = `${TEST_HOST}/note`;
beforeEach(() => {
- axiosMock.onDelete(endpoint).replyOnce(200, {});
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK, {});
document.body.dataset.page = '';
});
@@ -467,7 +473,7 @@ describe('Actions Notes Store', () => {
const endpoint = `${TEST_HOST}/note`;
beforeEach(() => {
- axiosMock.onDelete(endpoint).replyOnce(200, {});
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK, {});
document.body.dataset.page = '';
});
@@ -507,7 +513,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
});
it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', () => {
@@ -542,7 +548,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().replyOnce(200, res);
+ axiosMock.onAny().replyOnce(HTTP_STATUS_OK, res);
});
it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', () => {
@@ -563,7 +569,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
});
describe('as note', () => {
@@ -754,7 +760,7 @@ describe('Actions Notes Store', () => {
it('updates discussion if response contains disussion', () => {
const discussion = { notes: [] };
- axiosMock.onAny().reply(200, { discussion });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
actions.replyToDiscussion,
@@ -773,7 +779,7 @@ describe('Actions Notes Store', () => {
it('adds a reply to a discussion', () => {
const res = {};
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.replyToDiscussion,
@@ -1186,7 +1192,7 @@ describe('Actions Notes Store', () => {
describe('if response contains no errors', () => {
it('dispatches requestDeleteDescriptionVersion', () => {
- axiosMock.onDelete(endpoint).replyOnce(200);
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK);
return testAction(
actions.softDeleteDescriptionVersion,
payload,
@@ -1208,7 +1214,7 @@ describe('Actions Notes Store', () => {
describe('if response contains errors', () => {
const errorMessage = 'Request failed with status code 503';
it('dispatches receiveDeleteDescriptionVersionError and throws an error', async () => {
- axiosMock.onDelete(endpoint).replyOnce(503);
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
await expect(
testAction(
actions.softDeleteDescriptionVersion,
@@ -1438,7 +1444,7 @@ describe('Actions Notes Store', () => {
});
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => {
- axiosMock.onAny().reply(200, { discussion });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
actions.fetchDiscussions,
{},
@@ -1504,7 +1510,7 @@ describe('Actions Notes Store', () => {
const actionPayload = { config, path: 'test-path', perPage: 20 };
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', () => {
- axiosMock.onAny().reply(200, { discussion }, {});
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion }, {});
return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
@@ -1519,7 +1525,7 @@ describe('Actions Notes Store', () => {
});
it('dispatches itself if there is `x-next-page-cursor` header', () => {
- axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion }, { 'x-next-page-cursor': 1 });
return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
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 329cc15df97..601f8abd34d 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -17,6 +17,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
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';
+import { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -92,7 +93,7 @@ describe('DependencyProxyApp', () => {
window.gon = { ...dummyGon };
mock = new MockAdapter(axios);
- mock.onDelete(expectedUrl).reply(202, {});
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {});
});
afterEach(() => {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index a33528d2d91..801cde8582e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -30,6 +30,7 @@ exports[`packages_list_app renders 1`] = `
<div
class="gl-max-w-full gl-m-auto"
+ data-testid="gl-empty-state-content"
>
<div
class="gl-mx-auto gl-my-0 gl-p-5"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
index 36417eaf793..2c185e040f4 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/flash';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
@@ -182,7 +183,7 @@ describe('Actions Package list store', () => {
},
};
it('should perform a delete operation on _links.delete_api_path', () => {
- mock.onDelete(payload._links.delete_api_path).replyOnce(200);
+ mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_OK);
Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' });
return testAction(
@@ -198,7 +199,7 @@ describe('Actions Package list store', () => {
});
it('should stop the loading and call create flash on api error', async () => {
- mock.onDelete(payload._links.delete_api_path).replyOnce(400);
+ mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.requestDeletePackage,
payload,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
index 91824dee5b0..08e2de6c18f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
@@ -101,18 +101,6 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
+ <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
deleted file mode 100644
index bdd0fe3ad9e..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
+++ /dev/null
@@ -1,104 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`VersionRow renders 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
->
- <div
- class="gl-display-flex gl-align-items-center gl-py-3"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
- >
- <div
- class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
- >
- <gl-link-stub
- class="gl-text-body gl-min-w-0"
- href="243"
- >
- <span
- class="gl-truncate"
- data-testid="truncate-end-container"
- title="@gitlab-org/package-15"
- >
- <span
- class="gl-truncate-end"
- >
- @gitlab-org/package-15
- </span>
- </span>
- </gl-link-stub>
-
- <package-tags-stub
- class="gl-ml-3"
- hidelabel="true"
- tagdisplaylimit="1"
- tags="[object Object],[object Object],[object Object]"
- />
- </div>
-
- <!---->
- </div>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
- >
-
- 1.0.1
-
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
- >
- <publish-method-stub
- packageentity="[object Object]"
- />
- </div>
-
- <div
- class="gl-display-flex gl-align-items-center gl-min-h-6"
- >
- <span>
- Created
- <time-ago-tooltip-stub
- cssclass=""
- time="2021-08-10T09:33:54Z"
- tooltipplacement="top"
- />
- </span>
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index 20a459e2c1a..27c0ab96cfc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,8 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import Tracking from '~/tracking';
+import {
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
import { packageData } from '../../mock_data';
describe('PackageVersionsList', () => {
@@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow),
+ findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
};
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, {
@@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
},
stubs: {
RegistryList,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
},
slots: {
'empty-state': EmptySlotStub,
@@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
+
+ describe('when the user can bulk destroy versions', () => {
+ let eventSpy;
+ const { findDeletePackagesModal, findRegistryList } = uiElements;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent({ canDestroy: true });
+ });
+
+ it('binds the right props', () => {
+ expect(uiElements.findRegistryList().props()).toMatchObject({
+ items: packageList,
+ pagination: {},
+ isLoading: false,
+ hiddenDelete: false,
+ title: '2 versions',
+ });
+ });
+
+ describe('upon deletion', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', packageList);
+ });
+
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
+ expect(wrapper.emitted('delete')).toBeUndefined();
+ });
+
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findDeletePackagesModal().vm.$emit('confirm');
+ });
+
+ it('emits delete event', () => {
+ expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
+ });
+
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['confirm', 'cancel'])(
+ 'resets itemsToBeDeleted when modal emits %s',
+ async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ },
+ );
+
+ it('canceling delete tracks the right action', () => {
+ findDeletePackagesModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index faeca76d746..67340822fa5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,11 +1,13 @@
-import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { packageVersions } from '../../mock_data';
@@ -19,17 +21,23 @@ describe('VersionRow', () => {
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
+ const findPackageName = () => wrapper.findComponent(GlTruncate);
+ const findWarningIcon = () => wrapper.findComponent(GlIcon);
+ const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
- function createComponent(packageEntity = packageVersion) {
+ function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
propsData: {
packageEntity,
+ selected,
},
stubs: {
- ListItem,
GlSprintf,
GlTruncate,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
}
@@ -37,16 +45,15 @@ describe('VersionRow', () => {
wrapper.destroy();
});
- it('renders', () => {
+ it('has a link to the version detail', () => {
createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`);
});
- it('has a link to the version detail', () => {
+ it('lists the package name', () => {
createComponent();
- expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`);
expect(findLink().text()).toBe(packageVersion.name);
});
@@ -73,17 +80,89 @@ describe('VersionRow', () => {
expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt);
});
- describe('disabled status', () => {
- it('disables the list item', () => {
- createComponent({ ...packageVersion, status: 'something' });
+ describe('left action template', () => {
+ it('does not render checkbox if not permitted', () => {
+ createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
+
+ expect(findBulkDeleteAction().exists()).toBe(false);
+ });
+
+ it('renders checkbox', () => {
+ createComponent();
+
+ expect(findBulkDeleteAction().exists()).toBe(true);
+ expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
+ });
+
+ it('emits select when checked', () => {
+ createComponent();
+
+ findBulkDeleteAction().vm.$emit('change');
+
+ expect(wrapper.emitted('select')).toHaveLength(1);
+ });
+
+ it('renders checkbox in selected state if selected', () => {
+ createComponent({
+ selected: true,
+ });
+
+ expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props('selected')).toBe(true);
+ });
+ });
+
+ describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
+ beforeEach(() => {
+ createComponent({
+ packageEntity: {
+ ...packageVersion,
+ status: PACKAGE_ERROR_STATUS,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
+ });
+
+ it('lists the package name', () => {
+ expect(findPackageName().props('text')).toBe('@gitlab-org/package-15');
+ });
+
+ it('does not have a link to navigate to the details page', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+
+ it('has a warning icon', () => {
+ const icon = findWarningIcon();
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+ expect(icon.props('name')).toBe('warning');
+ expect(icon.props('ariaLabel')).toBe('Warning');
+ expect(tooltip.value).toMatchObject({
+ title: 'Invalid Package: failed metadata extraction',
+ });
+ });
+ });
- expect(findListItem().props('disabled')).toBe(true);
+ describe('disabled status', () => {
+ beforeEach(() => {
+ createComponent({
+ packageEntity: {
+ ...packageVersion,
+ status: 'something',
+ _links: {
+ webPath: null,
+ },
+ },
+ });
});
- it('disables the link', () => {
- createComponent({ ...packageVersion, status: 'something' });
+ it('lists the package name', () => {
+ expect(findPackageName().props('text')).toBe('@gitlab-org/package-15');
+ });
- expect(findLink().attributes('disabled')).toBe('true');
+ it('does not have a link to navigate to the details page', () => {
+ expect(findLink().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
index 93c2196b210..689b53fa2a4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
@@ -4,36 +4,38 @@ import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
-import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import {
- packageDestroyMutation,
- packageDestroyMutationError,
+ packagesDestroyMutation,
+ packagesDestroyMutationError,
packagesListQuery,
} from '../../mock_data';
jest.mock('~/flash');
-describe('DeletePackage', () => {
+describe('DeletePackages', () => {
let wrapper;
let apolloProvider;
let resolver;
let mutationResolver;
- const eventPayload = { id: '1' };
+ const eventPayload = [{ id: '1' }];
+ const eventPayloadMultiple = [{ id: '1' }, { id: '2' }];
+ const mutationPayload = { ids: ['1'] };
function createComponent(propsData = {}) {
Vue.use(VueApollo);
const requestHandlers = [
[getPackagesQuery, resolver],
- [destroyPackageMutation, mutationResolver],
+ [destroyPackagesMutation, mutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMountExtended(DeletePackage, {
+ wrapper = shallowMountExtended(DeletePackages, {
propsData,
apolloProvider,
scopedSlots: {
@@ -43,7 +45,9 @@ describe('DeletePackage', () => {
'data-testid': 'trigger-button',
},
on: {
- click: props.deletePackage,
+ click: (payload) => {
+ return props.deletePackages(payload[0]);
+ },
},
});
},
@@ -54,23 +58,23 @@ describe('DeletePackage', () => {
const findButton = () => wrapper.findByTestId('trigger-button');
const clickOnButtonAndWait = (payload) => {
- findButton().trigger('click', payload);
+ findButton().trigger('click', [payload]);
return waitForPromises();
};
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(packagesListQuery());
- mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation());
+ mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
});
afterEach(() => {
wrapper.destroy();
});
- it('binds deletePackage method to the default slot', () => {
+ it('binds deletePackages method to the default slot', () => {
createComponent();
- findButton().trigger('click');
+ findButton().trigger('click', eventPayload);
expect(wrapper.emitted('start')).toEqual([[]]);
});
@@ -80,7 +84,7 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
- expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(mutationResolver).toHaveBeenCalledWith(mutationPayload);
});
it('passes refetchQueries to apollo mutate', async () => {
@@ -91,10 +95,20 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
- expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(mutationResolver).toHaveBeenCalledWith(mutationPayload);
expect(resolver).toHaveBeenCalledWith(variables);
});
+ describe('when payload contains multiple packages', () => {
+ it('calls apollo mutation with different payload', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(mutationResolver).toHaveBeenCalledWith({ ids: ['1', '2'] });
+ });
+ });
+
describe('on mutation success', () => {
it('emits end event', async () => {
createComponent();
@@ -118,16 +132,29 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
expect(createAlert).toHaveBeenCalledWith({
- message: DeletePackage.i18n.successMessage,
+ message: DeletePackages.i18n.successMessage,
variant: VARIANT_SUCCESS,
});
});
+
+ describe('when payload contains multiple packages', () => {
+ it('calls createAlert with success message when showSuccessAlert is true', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DeletePackages.i18n.successMessageMultiple,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+ });
});
describe.each`
errorType | mutationResolverResponse
${'connectionError'} | ${jest.fn().mockRejectedValue()}
- ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())}
+ ${'localError'} | ${jest.fn().mockResolvedValue(packagesDestroyMutationError())}
`('on mutation $errorType', ({ mutationResolverResponse }) => {
beforeEach(() => {
mutationResolver = mutationResolverResponse;
@@ -147,11 +174,26 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
expect(createAlert).toHaveBeenCalledWith({
- message: DeletePackage.i18n.errorMessage,
+ message: DeletePackages.i18n.errorMessage,
variant: VARIANT_WARNING,
captureError: true,
error: expect.any(Error),
});
});
+
+ describe('when payload contains multiple packages', () => {
+ it('calls createAlert with error message', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DeletePackages.i18n.errorMessageMultiple,
+ variant: VARIANT_WARNING,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index a7de751aadd..ec8e77fa923 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -54,12 +54,15 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
>
<div
- class="gl-display-flex"
+ class="gl-display-flex gl-align-items-center"
data-testid="left-secondary-infos"
>
- <span>
- 1.0.0
- </span>
+ <gl-truncate-stub
+ class="gl-max-w-15 gl-md-max-w-26"
+ position="end"
+ text="1.0.0"
+ withtooltip="true"
+ />
<!---->
@@ -135,18 +138,6 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
+ <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index bb04701a8b7..2a78cfb13f9 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
@@ -43,9 +43,11 @@ describe('packages_list_row', () => {
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
+ const findPackageVersion = () => findLeftSecondaryInfos().findComponent(GlTruncate);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@@ -83,22 +85,13 @@ describe('packages_list_row', () => {
mountComponent();
expect(findPackageLink().props()).toMatchObject({
- event: 'click',
to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
});
});
- it('does not have a link to navigate to the details page', () => {
- mountComponent({
- packageEntity: {
- ...packageWithoutTags,
- _links: {
- webPath: null,
- },
- },
- });
+ it('lists the package name', () => {
+ mountComponent();
- expect(findPackageLink().exists()).toBe(false);
expect(findPackageName().props()).toMatchObject({
text: '@gitlab-org/package-15',
});
@@ -155,11 +148,25 @@ describe('packages_list_row', () => {
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
- mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
+ mountComponent({
+ packageEntity: {
+ ...packageWithoutTags,
+ status: PACKAGE_ERROR_STATUS,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
});
- it('details link is disabled', () => {
- expect(findPackageLink().props('event')).toBe('');
+ it('lists the package name', () => {
+ expect(findPackageName().props()).toMatchObject({
+ text: '@gitlab-org/package-15',
+ });
+ });
+
+ it('does not have a link to navigate to the details page', () => {
+ expect(findPackageLink().exists()).toBe(false);
});
it('has a warning icon', () => {
@@ -206,6 +213,9 @@ describe('packages_list_row', () => {
});
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props()).toMatchObject({
+ selected: true,
+ });
});
});
@@ -213,7 +223,10 @@ describe('packages_list_row', () => {
it('has the package version', () => {
mountComponent();
- expect(findLeftSecondaryInfos().text()).toContain(packageWithoutTags.version);
+ expect(findPackageVersion().props()).toMatchObject({
+ text: packageWithoutTags.version,
+ withTooltip: true,
+ });
});
it('if the pipeline exists show the author message', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 5e9cb8fbb0b..610640e0ca3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -167,8 +167,8 @@ describe('packages_list', () => {
findPackageListDeleteModal().vm.$emit('ok');
});
- it('emits package:delete when modal confirms', () => {
- expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
+ it('emits delete when modal confirms', () => {
+ expect(wrapper.emitted('delete')[0][0]).toEqual([firstPackage]);
});
it('tracks the right action', () => {
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 9e9e08bc196..d897be1f344 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -97,14 +97,22 @@ export const packageProject = () => ({
__typename: 'Project',
});
+export const linksData = {
+ _links: {
+ webPath: '/gitlab-org/package-15',
+ },
+};
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
+ ...linksData,
__typename: 'Package',
},
{
@@ -112,19 +120,14 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
+ ...linksData,
__typename: 'Package',
},
];
-export const linksData = {
- _links: {
- webPath: '/gitlab-org/package-15',
- __typeName: 'PackageLinks',
- },
-};
-
export const packageData = (extend) => ({
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',
@@ -294,14 +297,6 @@ export const packageMetadataQuery = (packageType) => {
};
};
-export const packageDestroyMutation = () => ({
- data: {
- destroyPackage: {
- errors: [],
- },
- },
-});
-
export const packagesDestroyMutation = () => ({
data: {
destroyPackages: {
@@ -329,25 +324,6 @@ export const packagesDestroyMutationError = () => ({
],
});
-export const packageDestroyMutationError = () => ({
- data: {
- destroyPackage: null,
- },
- errors: [
- {
- message:
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
- locations: [
- {
- line: 2,
- column: 3,
- },
- ],
- path: ['destroyPackage'],
- },
- ],
-});
-
export const packageDestroyFilesMutation = () => ({
data: {
destroyPackageFiles: {
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 eb3b999c1ca..b494965a3cb 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
@@ -15,7 +15,7 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
@@ -85,7 +85,7 @@ describe('PackagesApp', () => {
provide,
stubs: {
PackageTitle,
- DeletePackage,
+ DeletePackages,
GlModal: {
template: `
<div>
@@ -128,7 +128,8 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
- const findDeletePackage = () => wrapper.findComponent(DeletePackage);
+ const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
+ const findDeletePackages = () => wrapper.findComponent(DeletePackages);
afterEach(() => {
wrapper.destroy();
@@ -267,7 +268,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackage().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@@ -281,7 +282,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackage().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
@@ -595,13 +596,56 @@ describe('PackagesApp', () => {
it('binds the correct props', async () => {
const versionNodes = packageVersions();
- createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
+ createComponent();
+
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
+ canDestroy: true,
versions: expect.arrayContaining(versionNodes),
});
});
+
+ describe('delete packages', () => {
+ it('exists and has the correct props', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeletePackages().props()).toMatchObject({
+ refetchQueries: [{ query: getPackageDetails, variables: {} }],
+ showSuccessAlert: true,
+ });
+ });
+
+ it('deletePackages is bound to package-versions-list delete event', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findVersionsList().vm.$emit('delete', [{ id: 1 }]);
+
+ expect(findDeletePackages().emitted('start')).toEqual([[]]);
+ });
+
+ it('start and end event set loading correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findDeletePackages().vm.$emit('start');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(true);
+
+ findDeletePackages().vm.$emit('end');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(false);
+ });
+ });
});
describe('dependency links', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index b3cbd9f5dcf..a2ec527ce12 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,26 +8,18 @@ import ListPage from '~/packages_and_registries/package_registry/pages/list.vue'
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
-import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
- DELETE_PACKAGES_ERROR_MESSAGE,
- DELETE_PACKAGES_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
-import {
- packagesListQuery,
- packageData,
- pagination,
- packagesDestroyMutation,
- packagesDestroyMutationError,
-} from '../mock_data';
+import { packagesListQuery, packageData, pagination } from '../mock_data';
jest.mock('~/flash');
@@ -53,12 +45,11 @@ describe('PackagesListApp', () => {
filters: { packageName: 'foo', packageType: 'CONAN' },
};
- const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findDeletePackage = () => wrapper.findComponent(DeletePackage);
+ const findDeletePackages = () => wrapper.findComponent(DeletePackages);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -82,7 +73,7 @@ describe('PackagesListApp', () => {
GlSprintf,
GlLink,
PackageList,
- DeletePackage,
+ DeletePackages,
},
});
};
@@ -243,26 +234,26 @@ describe('PackagesListApp', () => {
});
});
- describe('delete package', () => {
+ describe('delete packages', () => {
it('exists and has the correct props', async () => {
mountComponent();
await waitForFirstRequest();
- expect(findDeletePackage().props()).toMatchObject({
+ expect(findDeletePackages().props()).toMatchObject({
refetchQueries: [{ query: getPackagesQuery, variables: {} }],
showSuccessAlert: true,
});
});
- it('deletePackage is bound to package-list package:delete event', async () => {
+ it('deletePackages is bound to package-list delete event', async () => {
mountComponent();
await waitForFirstRequest();
- findListComponent().vm.$emit('package:delete', { id: 1 });
+ findListComponent().vm.$emit('delete', [{ id: 1 }]);
- expect(findDeletePackage().emitted('start')).toEqual([[]]);
+ expect(findDeletePackages().emitted('start')).toEqual([[]]);
});
it('start and end event set loading correctly', async () => {
@@ -270,59 +261,17 @@ describe('PackagesListApp', () => {
await waitForFirstRequest();
- findDeletePackage().vm.$emit('start');
+ findDeletePackages().vm.$emit('start');
await nextTick();
expect(findListComponent().props('isLoading')).toBe(true);
- findDeletePackage().vm.$emit('end');
+ findDeletePackages().vm.$emit('end');
await nextTick();
expect(findListComponent().props('isLoading')).toBe(false);
});
});
-
- describe('bulk delete package', () => {
- const items = [{ id: '1' }, { id: '2' }];
-
- it('calls mutation with the right values and shows success alert', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
- mountComponent({
- mutationResolver,
- });
-
- await waitForFirstRequest();
-
- findListComponent().vm.$emit('delete', items);
-
- expect(mutationResolver).toHaveBeenCalledWith({
- ids: items.map((item) => item.id),
- });
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().props('variant')).toEqual('success');
- expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_SUCCESS_MESSAGE);
- });
-
- it('on error shows danger alert', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutationError());
- mountComponent({
- mutationResolver,
- });
-
- await waitForFirstRequest();
-
- findListComponent().vm.$emit('delete', items);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().props('variant')).toEqual('danger');
- expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_ERROR_MESSAGE);
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
index daf0ee85fdf..0fbbf4ae58f 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -11,6 +11,7 @@ import {
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
} from '~/packages_and_registries/settings/project/constants';
+import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
import Tracking from '~/tracking';
import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data';
@@ -39,10 +40,13 @@ describe('Packages Cleanup Policy Settings Form', () => {
label: 'packages_cleanup_policies',
};
+ const defaultQueryResolver = jest.fn().mockResolvedValue(packagesCleanupPolicyPayload());
+
const findForm = () => wrapper.findComponent({ ref: 'form-element' });
const findSaveButton = () => wrapper.findByTestId('save-button');
const findKeepNDuplicatedPackageFilesDropdown = () =>
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
+ const findNextRunAt = () => wrapper.findByTestId('next-run-at');
const submitForm = async () => {
findForm().trigger('submit');
@@ -77,10 +81,14 @@ describe('Packages Cleanup Policy Settings Form', () => {
const mountComponentWithApollo = ({
provide = defaultProvidedValues,
+ queryResolver = defaultQueryResolver,
mutationResolver,
queryPayload = packagesCleanupPolicyPayload(),
} = {}) => {
- const requestHandlers = [[updatePackagesCleanupPolicyMutation, mutationResolver]];
+ const requestHandlers = [
+ [updatePackagesCleanupPolicyMutation, mutationResolver],
+ [packagesCleanupPolicyQuery, queryResolver],
+ ];
fakeApollo = createMockApollo(requestHandlers);
@@ -160,6 +168,40 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
});
+ describe('nextRunAt', () => {
+ it('when present renders time until next package cleanup', () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: '2063-04-04T02:42:00Z' } },
+ });
+
+ expect(findNextRunAt().text()).toMatchInterpolatedText(
+ 'Packages and assets will not be deleted until cleanup runs in about 2 hours.',
+ );
+ });
+
+ it('renders message for cleanup when its before current date', () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: '2063-03-04T00:42:00Z' } },
+ });
+
+ expect(findNextRunAt().text()).toMatchInterpolatedText(
+ 'Packages and assets cleanup is ready to be executed when the next cleanup job runs.',
+ );
+ });
+
+ it('when null hides time until next package cleanup', () => {
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: null } },
+ });
+
+ expect(findNextRunAt().exists()).toBe(false);
+ });
+ });
+
describe('form', () => {
describe('actions', () => {
describe('submit button', () => {
@@ -209,7 +251,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
describe('form submit event', () => {
- it('dispatches the correct apollo mutation', () => {
+ it('dispatches the correct apollo mutation and refetches query', async () => {
const mutationResolver = jest
.fn()
.mockResolvedValue(packagesCleanupPolicyMutationPayload());
@@ -225,6 +267,12 @@ describe('Packages Cleanup Policy Settings Form', () => {
projectPath: 'path',
},
});
+
+ await waitForPromises();
+
+ expect(defaultQueryResolver).toHaveBeenCalledWith({
+ projectPath: 'path',
+ });
});
it('tracks the submit event', () => {
@@ -251,6 +299,18 @@ describe('Packages Cleanup Policy Settings Form', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
});
+ it('shows error toast when mutation responds with errors', async () => {
+ mountComponentWithApollo({
+ mutationResolver: jest
+ .fn()
+ .mockResolvedValue(packagesCleanupPolicyMutationPayload({ errors: [new Error()] })),
+ });
+
+ await submitForm();
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
+ });
+
describe('when submit fails', () => {
it('shows an error', async () => {
mountComponentWithApollo({
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
index aaca58d21bb..2e2d5e26d33 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
@@ -54,6 +54,28 @@ describe('Registry List', () => {
it('exists', () => {
expect(findSelectAll().exists()).toBe(true);
+ expect(findSelectAll().attributes('aria-label')).toBe('Select all');
+ expect(findSelectAll().attributes('disabled')).toBeUndefined();
+ expect(findSelectAll().attributes('indeterminate')).toBeUndefined();
+ });
+
+ it('sets disabled prop to true when items length is 0', () => {
+ mountComponent({ propsData: { ...defaultPropsData, items: [] } });
+
+ expect(findSelectAll().attributes('disabled')).toBe('true');
+ });
+
+ it('when few are selected, sets indeterminate prop to true', async () => {
+ await findScopedSlotSelectButton(0).trigger('click');
+
+ expect(findSelectAll().attributes('indeterminate')).toBe('true');
+ });
+
+ it('when all are selected, sets the right checkbox label', async () => {
+ findSelectAll().vm.$emit('change', true);
+ await nextTick();
+
+ expect(findSelectAll().attributes('aria-label')).toBe('Unselect all');
});
it('select and unselect all', async () => {
@@ -63,7 +85,7 @@ describe('Registry List', () => {
});
// simulate selection
- findSelectAll().vm.$emit('input', true);
+ findSelectAll().vm.$emit('change', true);
await nextTick();
// all rows selected
@@ -72,12 +94,12 @@ describe('Registry List', () => {
});
// simulate de-selection
- findSelectAll().vm.$emit('input', '');
+ findSelectAll().vm.$emit('change', false);
await nextTick();
// no row is not selected
items.forEach((item, index) => {
- expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
+ expect(findScopedSlotIsSelectedValue(index).text()).toBe('false');
});
});
});
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index dfb3e87a342..2904ef547fe 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { removeParams } from '~/lib/utils/url_utility';
import Pager from '~/pager';
@@ -49,7 +50,7 @@ describe('pager', () => {
const urlRegex = /(.*)some_list(.*)$/;
function mockSuccess(count = 0) {
- axiosMock.onGet(urlRegex).reply(200, {
+ axiosMock.onGet(urlRegex).reply(HTTP_STATUS_OK, {
count,
html: '',
});
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 85ed94b748d..d422f5dade3 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -1,20 +1,15 @@
-import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import initUserInternalRegexPlaceholder, {
+import initAccountAndLimitsSection, {
PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE,
PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE,
} from '~/pages/admin/application_settings/account_and_limits';
describe('AccountAndLimits', () => {
const FIXTURE = 'application_settings/accounts_and_limit.html';
- let $userDefaultExternal;
- let $userInternalRegex;
beforeEach(() => {
loadHTMLFixture(FIXTURE);
- initUserInternalRegexPlaceholder();
- $userDefaultExternal = $('#application_setting_user_default_external');
- $userInternalRegex = document.querySelector('#application_setting_user_default_internal_regex');
+ initAccountAndLimitsSection();
});
afterEach(() => {
@@ -22,18 +17,62 @@ describe('AccountAndLimits', () => {
});
describe('Changing of userInternalRegex when userDefaultExternal', () => {
+ /** @type {HTMLInputElement} */
+ let userDefaultExternalCheckbox;
+ /** @type {HTMLInputElement} */
+ let userInternalRegexInput;
+
+ beforeEach(() => {
+ userDefaultExternalCheckbox = document.getElementById(
+ 'application_setting_user_default_external',
+ );
+ userInternalRegexInput = document.getElementById(
+ 'application_setting_user_default_internal_regex',
+ );
+ });
+
it('is unchecked', () => {
- expect($userDefaultExternal.prop('checked')).toBe(false);
- expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE);
- expect($userInternalRegex.readOnly).toBe(true);
+ expect(userDefaultExternalCheckbox.checked).toBe(false);
+ expect(userInternalRegexInput.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE);
+ expect(userInternalRegexInput.readOnly).toBe(true);
});
it('is checked', () => {
- if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click();
+ if (!userDefaultExternalCheckbox.checked) userDefaultExternalCheckbox.click();
+
+ expect(userDefaultExternalCheckbox.checked).toBe(true);
+ expect(userInternalRegexInput.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
+ expect(userInternalRegexInput.readOnly).toBe(false);
+ });
+ });
+
+ describe('Dormant users period input logic', () => {
+ /** @type {HTMLInputElement} */
+ let checkbox;
+ /** @type {HTMLInputElement} */
+ let input;
+
+ const updateCheckbox = (checked) => {
+ checkbox.checked = checked;
+ checkbox.dispatchEvent(new Event('change'));
+ };
+
+ beforeEach(() => {
+ checkbox = document.getElementById('application_setting_deactivate_dormant_users');
+ input = document.getElementById('application_setting_deactivate_dormant_users_period');
+ });
+
+ it('initial state', () => {
+ expect(checkbox.checked).toBe(false);
+ expect(input.disabled).toBe(true);
+ });
+
+ it('changes field enabled flag on checkbox change', () => {
+ updateCheckbox(true);
+ expect(input.disabled).toBe(false);
- expect($userDefaultExternal.prop('checked')).toBe(true);
- expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
- expect($userInternalRegex.readOnly).toBe(false);
+ updateCheckbox(false);
+ expect(input.disabled).toBe(true);
});
});
});
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
index 17669331370..366d148a608 100644
--- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
@@ -4,30 +4,27 @@ import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
-import StopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
+import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
redirectTo: jest.fn(),
}));
-describe('stop_jobs_modal.vue', () => {
+describe('Cancel jobs modal', () => {
const props = {
- url: `${TEST_HOST}/stop_jobs_modal.vue/stopAll`,
+ url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
+ modalId: 'cancel-jobs-modal',
};
let wrapper;
beforeEach(() => {
- wrapper = mount(StopJobsModal, { propsData: props });
- });
-
- afterEach(() => {
- wrapper.destroy();
+ wrapper = mount(CancelJobsModal, { propsData: props });
});
describe('on submit', () => {
- it('stops jobs and redirects to overview page', async () => {
- const responseURL = `${TEST_HOST}/stop_jobs_modal.vue/jobs`;
+ it('cancels jobs and redirects to overview page', async () => {
+ const responseURL = `${TEST_HOST}/cancel_jobs_modal.vue/jobs`;
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
//
@@ -47,10 +44,10 @@ describe('stop_jobs_modal.vue', () => {
expect(redirectTo).toHaveBeenCalledWith(responseURL);
});
- it('displays error if stopping jobs failed', async () => {
+ it('displays error if canceling jobs failed', async () => {
Vue.config.errorHandler = () => {}; // silencing thrown error
- const dummyError = new Error('stopping jobs failed');
+ const dummyError = new Error('canceling jobs failed');
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
//
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
new file mode 100644
index 00000000000..ec6369e7119
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
@@ -0,0 +1,57 @@
+import { GlButton } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { TEST_HOST } from 'helpers/test_constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CancelJobs from '~/pages/admin/jobs/index/components/cancel_jobs.vue';
+import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
+import {
+ CANCEL_JOBS_MODAL_ID,
+ CANCEL_BUTTON_TOOLTIP,
+} from '~/pages/admin/jobs/index/components/constants';
+
+describe('CancelJobs component', () => {
+ let wrapper;
+
+ const findCancelJobs = () => wrapper.findComponent(CancelJobs);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(CancelJobsModal);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(CancelJobs, {
+ directives: {
+ GlModal: createMockDirective(),
+ GlTooltip: createMockDirective(),
+ },
+ propsData: {
+ url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has correct inputs', () => {
+ expect(findCancelJobs().props().url).toBe(`${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`);
+ });
+
+ it('has correct button variant', () => {
+ expect(findButton().props().variant).toBe('danger');
+ });
+
+ it('checks that button and modal are connected', () => {
+ const buttonModalDirective = getBinding(findButton().element, 'gl-modal');
+ const modalId = findModal().props('modalId');
+
+ expect(buttonModalDirective.value).toBe(CANCEL_JOBS_MODAL_ID);
+ expect(modalId).toBe(CANCEL_JOBS_MODAL_ID);
+ });
+
+ it('checks that tooltip is displayed', () => {
+ const buttonTooltipDirective = getBinding(findButton().element, 'gl-tooltip');
+
+ expect(buttonTooltipDirective.value).toBe(CANCEL_BUTTON_TOOLTIP);
+ });
+});
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index 909349569a8..834d14e0fb3 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -1,99 +1,142 @@
-import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
-describe('Dropdown select component', () => {
+const TEST_USER_NAMESPACE = { id: 10, kind: 'user', full_path: 'Administrator' };
+const TEST_GROUP_NAMESPACE = { id: 20, kind: 'group', full_path: 'GitLab Org' };
+
+describe('NamespaceSelect', () => {
let wrapper;
- const mountDropdown = (propsData) => {
- wrapper = mount(NamespaceSelect, { propsData });
+ const createComponent = (propsData) => {
+ wrapper = shallowMountExtended(NamespaceSelect, { propsData });
};
- const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
- const findNamespaceInput = () => wrapper.find('[data-testid="hidden-input"]');
- const findFilterInput = () => wrapper.find('.namespace-search-box input');
- const findDropdownOption = (match) => {
- const buttons = wrapper
- .findAll('button.dropdown-item')
- .filter((node) => node.text().match(match));
- return buttons.length ? buttons.at(0) : buttons;
- };
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findNamespaceInput = () => wrapper.findByTestId('hidden-input');
- const setFieldValue = async (field, value) => {
- await field.setValue(value);
- field.trigger('blur');
+ const search = async (searchString) => {
+ findListbox().vm.$emit('search', searchString);
+ await waitForPromises();
};
beforeEach(() => {
setHTMLFixture('<div class="test-container"></div>');
- jest.spyOn(Api, 'namespaces').mockImplementation((_, callback) =>
- callback([
- { id: 10, kind: 'user', full_path: 'Administrator' },
- { id: 20, kind: 'group', full_path: 'GitLab Org' },
- ]),
- );
+ jest
+ .spyOn(Api, 'namespaces')
+ .mockImplementation((_, callback) => callback([TEST_USER_NAMESPACE, TEST_GROUP_NAMESPACE]));
});
afterEach(() => {
resetHTMLFixture();
});
- it('creates a hidden input if fieldName is provided', () => {
- mountDropdown({ fieldName: 'namespace-input' });
+ describe('on mount', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not show hidden input', () => {
+ expect(findNamespaceInput().exists()).toBe(false);
+ });
+
+ it('sets appropriate props', async () => {
+ expect(findListbox().props()).toMatchObject({
+ items: [
+ { text: 'user: Administrator', value: '10' },
+ { text: 'group: GitLab Org', value: '20' },
+ ],
+ headerText: NamespaceSelect.i18n.headerText,
+ resetButtonLabel: NamespaceSelect.i18n.reset,
+ toggleText: 'Namespace',
+ searchPlaceholder: NamespaceSelect.i18n.searchPlaceholder,
+ searching: false,
+ searchable: true,
+ });
+ });
+ });
+
+ it('with fieldName, shows hidden input', () => {
+ createComponent({ fieldName: 'namespace-input' });
expect(findNamespaceInput().exists()).toBe(true);
expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
});
- describe('clicking dropdown options', () => {
+ describe('select', () => {
+ describe.each`
+ selectId | expectToggleText
+ ${String(TEST_USER_NAMESPACE.id)} | ${`user: ${TEST_USER_NAMESPACE.full_path}`}
+ ${String(TEST_GROUP_NAMESPACE.id)} | ${`group: ${TEST_GROUP_NAMESPACE.full_path}`}
+ `('clicking listbox options (selectId=$selectId)', ({ selectId, expectToggleText }) => {
+ beforeEach(async () => {
+ createComponent({ fieldName: 'namespace-input' });
+ findListbox().vm.$emit('select', selectId);
+ await nextTick();
+ });
+
+ it('updates hidden field', () => {
+ expect(findNamespaceInput().attributes('value')).toBe(selectId);
+ });
+
+ it('updates the listbox value', async () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: selectId,
+ toggleText: expectToggleText,
+ });
+ });
+
+ it('triggers a setNamespace event upon selection', () => {
+ expect(wrapper.emitted('setNamespace')).toEqual([[selectId]]);
+ });
+ });
+ });
+
+ describe('search', () => {
it('retrieves namespaces based on filter query', async () => {
- mountDropdown();
+ createComponent();
- await setFieldValue(findFilterInput(), 'test');
+ // Add space to assert that `?.trim` is called
+ await search('test ');
expect(Api.namespaces).toHaveBeenCalledWith('test', expect.anything());
});
- it('updates the dropdown value based upon selection', async () => {
- mountDropdown({ fieldName: 'namespace-input' });
-
- // wait for dropdown options to populate
- await nextTick();
-
- expect(findDropdownOption('user: Administrator').exists()).toBe(true);
- expect(findDropdownOption('group: GitLab Org').exists()).toBe(true);
- expect(findDropdownOption('group: Foobar').exists()).toBe(false);
+ it('when not found, does not change the placeholder text', async () => {
+ createComponent({
+ origSelectedId: String(TEST_USER_NAMESPACE.id),
+ origSelectedText: `user: ${TEST_USER_NAMESPACE.full_path}`,
+ });
- findDropdownOption('user: Administrator').trigger('click');
- await nextTick();
+ await search('not exist');
- expect(findNamespaceInput().attributes('value')).toBe('10');
- expect(findDropdownToggle().text()).toBe('user: Administrator');
+ expect(findListbox().props()).toMatchObject({
+ selected: String(TEST_USER_NAMESPACE.id),
+ toggleText: `user: ${TEST_USER_NAMESPACE.full_path}`,
+ });
});
+ });
- it('triggers a setNamespace event upon selection', async () => {
- mountDropdown();
-
- // wait for dropdown options to populate
- await nextTick();
-
- findDropdownOption('group: GitLab Org').trigger('click');
-
- expect(wrapper.emitted('setNamespace')).toHaveLength(1);
- expect(wrapper.emitted('setNamespace')[0][0]).toBe(20);
+ describe('reset', () => {
+ beforeEach(() => {
+ createComponent();
+ findListbox().vm.$emit('reset');
});
- it('displays "Any Namespace" option when showAny prop provided', () => {
- mountDropdown({ showAny: true });
- expect(wrapper.text()).toContain('Any namespace');
+ it('updates the listbox value', () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: null,
+ toggleText: 'Namespace',
+ });
});
- it('does not display "Any Namespace" option when showAny prop not provided', () => {
- mountDropdown();
- expect(wrapper.text()).not.toContain('Any namespace');
+ it('triggers a setNamespace event upon reset', () => {
+ expect(wrapper.emitted('setNamespace')).toEqual([[null]]);
});
});
});
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 825aef27327..70d7cb9c839 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -3,6 +3,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { addDelimiter } from '~/lib/utils/text_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
@@ -41,7 +42,7 @@ describe('Todos', () => {
// Arrange
mock
.onDelete(path)
- .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
+ .replyOnce(HTTP_STATUS_OK, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
onToggleSpy = jest.fn();
document.addEventListener('todo:toggle', onToggleSpy);
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index 1a157beebe4..da3954b4918 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -2,6 +2,7 @@ import { GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
@@ -78,7 +79,7 @@ describe('BulkImportsHistoryApp', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
});
afterEach(() => {
@@ -93,7 +94,7 @@ describe('BulkImportsHistoryApp', () => {
});
it('renders empty state when no data is available', async () => {
- mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -167,7 +168,7 @@ describe('BulkImportsHistoryApp', () => {
it('renders loading icon when destination namespace is not defined', async () => {
const RESPONSE = [{ ...DUMMY_RESPONSE[0], destination_full_path: null }];
- mock.onGet(API_URL).reply(200, RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
await axios.waitForAll();
@@ -192,7 +193,7 @@ describe('BulkImportsHistoryApp', () => {
describe('details button', () => {
beforeEach(() => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
return axios.waitForAll();
});
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
index 82a3e11186e..628ee8d7999 100644
--- a/spec/frontend/pages/import/history/components/import_error_details_spec.js
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
describe('ImportErrorDetails', () => {
@@ -46,7 +47,7 @@ describe('ImportErrorDetails', () => {
it('renders import_error if it is available', async () => {
const FAKE_IMPORT_ERROR = 'IMPORT ERROR';
- mock.onGet(API_URL).reply(200, { import_error: FAKE_IMPORT_ERROR });
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, { import_error: FAKE_IMPORT_ERROR });
createComponent();
await axios.waitForAll();
@@ -55,7 +56,7 @@ describe('ImportErrorDetails', () => {
});
it('renders default text if error is not available', async () => {
- mock.onGet(API_URL).reply(200, { import_error: null });
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, { import_error: null });
createComponent();
await axios.waitForAll();
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
index 5030adae2fa..7d79583be19 100644
--- a/spec/frontend/pages/import/history/components/import_history_app_spec.js
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -2,6 +2,7 @@ import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
import ImportHistoryApp from '~/pages/import/history/components/import_history_app.vue';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
@@ -61,6 +62,7 @@ describe('ImportHistoryApp', () => {
const originalApiVersion = gon.api_version;
beforeAll(() => {
gon.api_version = 'v4';
+ gon.features = { fullPathProjectSearch: true };
});
afterAll(() => {
@@ -83,7 +85,7 @@ describe('ImportHistoryApp', () => {
});
it('renders empty state when no data is available', async () => {
- mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -92,7 +94,7 @@ describe('ImportHistoryApp', () => {
});
it('renders table with data when history is available', async () => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -104,7 +106,7 @@ describe('ImportHistoryApp', () => {
it('changes page when requested by pagination bar', async () => {
const NEW_PAGE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -120,7 +122,7 @@ describe('ImportHistoryApp', () => {
},
];
- mock.onGet(API_URL).reply(200, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
await axios.waitForAll();
@@ -134,7 +136,7 @@ describe('ImportHistoryApp', () => {
it('changes page size when requested by pagination bar', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -151,7 +153,7 @@ describe('ImportHistoryApp', () => {
it('resets page to 1 when page size is changed', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
@@ -169,7 +171,7 @@ describe('ImportHistoryApp', () => {
describe('details button', () => {
beforeEach(() => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
return axios.waitForAll();
});
diff --git a/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
new file mode 100644
index 00000000000..ef2e5d779d8
--- /dev/null
+++ b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
@@ -0,0 +1,39 @@
+import { generateRefDestinationPath } from '~/pages/projects/find_file/ref_switcher/ref_switcher_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const projectRootPath = 'root/Project1';
+const selectedRef = 'feature/test';
+
+describe('generateRefDestinationPath', () => {
+ it.each`
+ currentPath | result
+ ${`${projectRootPath}/-/find_file/flightjs/Flight`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}`}
+ ${`${projectRootPath}/-/find_file/test/test1?test=something`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something`}
+ ${`${projectRootPath}/-/find_file/simpletest?test=something&test=it`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test=it`}
+ ${`${projectRootPath}/-/find_file/some_random_char?test=something&test[]=it&test[]=is`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test[]=it&test[]=is`}
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(selectedRef, '/-/find_file')).toBe(result);
+ });
+
+ it("returns original url if it's missing selectedRef param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(undefined, '/-/find_file')).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+
+ it("returns original url if it's missing namespace param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(selectedRef, undefined)).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+
+ it("returns original url if it's missing namespace and selectedRef param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(undefined, undefined)).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+});
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 aee56247209..f0593a854b2 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
@@ -12,6 +12,7 @@ import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
+import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
jest.mock('~/flash');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -475,6 +476,43 @@ describe('ForkForm component', () => {
expect(axios.post).not.toHaveBeenCalled();
});
+
+ describe('project name', () => {
+ it.each`
+ value | expectedErrorMessage
+ ${'?'} | ${START_RULE.msg}
+ ${'*'} | ${START_RULE.msg}
+ ${'a?'} | ${CONTAINS_RULE.msg}
+ ${'a*'} | ${CONTAINS_RULE.msg}
+ `(
+ 'shows "$expectedErrorMessage" error when value is $value',
+ async ({ value, expectedErrorMessage }) => {
+ createFullComponent();
+
+ findForkNameInput().vm.$emit('input', value);
+ await nextTick();
+ await submitForm();
+
+ const formGroup = wrapper.findComponent('[data-testid="fork-name-form-group"]');
+
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe(expectedErrorMessage);
+ expect(formGroup.vm.$attrs.description).toBe(null);
+ },
+ );
+
+ it.each(['a', '9', 'aa', '99'])('does not show error when value is %s', async (value) => {
+ createFullComponent();
+
+ findForkNameInput().vm.$emit('input', value);
+ await nextTick();
+ await submitForm();
+
+ const formGroup = wrapper.findComponent('[data-testid="fork-name-form-group"]');
+
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe('');
+ expect(formGroup.vm.$attrs.description).not.toBe(null);
+ });
+ });
});
describe('with valid form', () => {
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
deleted file mode 100644
index d67f842d011..00000000000
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ /dev/null
@@ -1,109 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
-<div>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-t gl-pt-4 gl-mb-3"
- >
- <h4
- class="gl-m-0"
- sub-header=""
- >
- <gl-sprintf-stub
- message="Code coverage statistics for %{ref} %{start_date} - %{end_date}"
- />
- </h4>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- data-testid="download-button"
- href="url/"
- icon=""
- size="small"
- variant="default"
- >
-
- Download raw data (.csv)
-
- </gl-button-stub>
- </div>
-
- <div
- class="gl-mt-3 gl-mb-3"
- >
- <!---->
-
- <!---->
-
- <gl-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- size="medium"
- toggleid="dropdown-toggle-btn-6"
- toggletext="rspec"
- variant="default"
- >
-
- <!---->
-
- <!---->
-
- <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"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-0"
- isselected="true"
- >
-
- rspec
-
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-1"
- >
-
- cypress
-
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-2"
- >
-
- karma
-
- </gl-listbox-item-stub>
-
- <!---->
-
- <!---->
- </ul>
-
- <!---->
-
- </gl-base-dropdown-stub>
- </div>
-
- <gl-area-chart-stub
- annotations=""
- data="[object Object]"
- formattooltiptext="[Function]"
- height="200"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- responsive=""
- thresholds=""
- />
-</div>
-`;
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 2ff45266a07..5356953060a 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -68,10 +68,6 @@ describe('Code Coverage', () => {
expect(wrapper.vm.sortedData).toEqual(sortedDataByDates);
});
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('shows no error messages', () => {
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
deleted file mode 100644
index 83feb621478..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
+++ /dev/null
@@ -1,62 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Learn GitLab Section Card renders correctly 1`] = `
-<gl-card-stub
- bodyclass="gl-pt-0"
- class="gl-pt-0 h-100"
- footerclass=""
- headerclass="gl-bg-white gl-border-0 gl-pb-0"
->
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- <learn-gitlab-section-link-stub
- action="userAdded"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="issueCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="gitWrite"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="mergeRequestCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="securityScanEnabled"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="pipelineCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="trialStarted"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="codeOwnersEnabled"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="requiredMrApprovalsEnabled"
- value="[object Object]"
- />
-</gl-card-stub>
-`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
deleted file mode 100644
index 6b6833b00c3..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ /dev/null
@@ -1,409 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Learn GitLab renders correctly 1`] = `
-<div>
- <!---->
-
- <div
- class="row"
- >
- <div
- class="gl-mb-7 gl-ml-5"
- >
- <h1
- class="gl-font-size-h1"
- >
- Learn GitLab
- </h1>
-
- <p
- class="gl-text-gray-700 gl-mb-0"
- >
- Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.
- </p>
- </div>
- </div>
-
- <div
- class="gl-mb-3"
- >
- <p
- class="gl-text-gray-500 gl-mb-2"
- data-testid="completion-percentage"
- >
- 22% completed
- </p>
-
- <div
- class="progress"
- max="9"
- value="2"
- >
- <div
- aria-valuemax="9"
- aria-valuemin="0"
- aria-valuenow="2"
- class="progress-bar"
- role="progressbar"
- style="width: 22.22222222222222%;"
- />
- </div>
- </div>
-
- <div
- class="row"
- >
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <span
- class="gl-text-green-500"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Invite your colleagues
-
- <!---->
- </span>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <span
- class="gl-text-green-500"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Create a repository
-
- <!---->
- </span>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="set_up_your_first_project_s_ci_cd"
- href="http://example.com/"
- target="_self"
- >
- Set up your first project's CI/CD
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="start_a_free_trial_of_gitlab_ultimate"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Start a free trial of GitLab Ultimate
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="add_code_owners"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Add code owners
- </a>
-
- <span
- class="gl-font-style-italic gl-text-gray-500"
- data-testid="trial-only"
- >
-
- - Included in trial
-
- </span>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="enable_require_merge_approvals"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Enable require merge approvals
- </a>
-
- <span
- class="gl-font-style-italic gl-text-gray-500"
- data-testid="trial-only"
- >
-
- - Included in trial
-
- </span>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="plan.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Plan and execute
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Create a workflow for your new workspace, and learn how GitLab features work together:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="create_an_issue"
- href="http://example.com/"
- target="_self"
- >
- Create an issue
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="submit_a_merge_request_mr"
- href="http://example.com/"
- target="_self"
- >
- Submit a merge request (MR)
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="deploy.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Deploy
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="run_a_security_scan_using_ci_cd"
- href="https://docs.gitlab.com/ee/foobar/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Run a Security scan using CI/CD
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js
deleted file mode 100644
index 3a511a009a9..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue';
-import { testActions } from './mock_data';
-
-const defaultSection = 'workspace';
-const testImage = 'workspace.svg';
-
-describe('Learn GitLab Section Card', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const createWrapper = () => {
- wrapper = shallowMount(LearnGitlabSectionCard, {
- propsData: { section: defaultSection, actions: testActions, svg: testImage },
- });
- };
-
- it('renders correctly', () => {
- createWrapper({ completed: false });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
deleted file mode 100644
index 29335308370..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import { GlPopover, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { stubExperiments } from 'helpers/experimentation_helper';
-import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
-import eventHub from '~/invite_members/event_hub';
-import LearnGitlabSectionLink from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue';
-import { ACTION_LABELS } from '~/pages/projects/learn_gitlab/constants';
-
-const defaultAction = 'gitWrite';
-const defaultProps = {
- title: 'Create Repository',
- description: 'Some description',
- url: 'https://example.com',
- completed: false,
- enabled: true,
-};
-
-const openInNewTabProps = {
- url: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/',
- openInNewTab: true,
-};
-
-describe('Learn GitLab Section Link', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const createWrapper = (action = defaultAction, props = {}) => {
- wrapper = extendedWrapper(
- mount(LearnGitlabSectionLink, {
- propsData: { action, value: { ...defaultProps, ...props } },
- }),
- );
- };
-
- const openInviteMembesrModalLink = () =>
- wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
-
- const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
- const findDisabledLink = () => wrapper.findByTestId('disabled-learn-gitlab-link');
- const findPopoverTrigger = () => wrapper.findByTestId('contact-admin-popover-trigger');
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findPopoverLink = () => findPopover().findComponent(GlLink);
- const videoTutorialLink = () => wrapper.find('[data-testid="video-tutorial-link"]');
-
- it('renders no icon when not completed', () => {
- createWrapper(undefined, { completed: false });
-
- expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(false);
- });
-
- it('renders the completion icon when completed', () => {
- createWrapper(undefined, { completed: true });
-
- expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(true);
- });
-
- it('renders no trial only when it is not required', () => {
- createWrapper();
-
- expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(false);
- });
-
- it('renders trial only when trial is required', () => {
- createWrapper('codeOwnersEnabled');
-
- expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
- });
-
- describe('disabled links', () => {
- beforeEach(() => {
- createWrapper('trialStarted', { enabled: false });
- });
-
- it('renders text without a link', () => {
- expect(findDisabledLink().exists()).toBe(true);
- expect(findDisabledLink().text()).toBe(ACTION_LABELS.trialStarted.title);
- expect(findDisabledLink().attributes('href')).toBeUndefined();
- });
-
- it('renders a popover trigger with question icon', () => {
- expect(findPopoverTrigger().exists()).toBe(true);
- expect(findPopoverTrigger().props('icon')).toBe('question-o');
- expect(findPopoverTrigger().attributes('aria-label')).toBe(
- LearnGitlabSectionLink.i18n.contactAdmin,
- );
- });
-
- it('renders a popover', () => {
- expect(findPopoverTrigger().attributes('id')).toBe(findPopover().props('target'));
- expect(findPopover().props()).toMatchObject({
- placement: 'top',
- triggers: 'hover focus',
- });
- });
-
- it('renders default disabled message', () => {
- expect(findPopover().text()).toContain(LearnGitlabSectionLink.i18n.contactAdmin);
- });
-
- it('renders custom disabled message if provided', () => {
- createWrapper('trialStarted', { enabled: false, message: 'Custom message' });
- expect(findPopover().text()).toContain('Custom message');
- });
-
- it('renders a link inside the popover', () => {
- expect(findPopoverLink().exists()).toBe(true);
- expect(findPopoverLink().attributes('href')).toBe(defaultProps.url);
- });
- });
-
- describe('links marked with openInNewTab', () => {
- beforeEach(() => {
- createWrapper('securityScanEnabled', openInNewTabProps);
- });
-
- it('renders links with blank target', () => {
- const linkElement = findUncompletedLink();
-
- expect(linkElement.exists()).toBe(true);
- expect(linkElement.attributes('target')).toEqual('_blank');
- });
-
- it('tracks the click', () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- findUncompletedLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
- label: 'run_a_security_scan_using_ci_cd',
- });
-
- unmockTracking();
- });
- });
-
- describe('rendering a link to open the invite_members modal instead of a regular link', () => {
- it.each`
- action | experimentVariant | showModal
- ${'userAdded'} | ${'candidate'} | ${true}
- ${'userAdded'} | ${'control'} | ${false}
- ${defaultAction} | ${'candidate'} | ${false}
- ${defaultAction} | ${'control'} | ${false}
- `(
- 'when the invite_for_help_continuous_onboarding experiment has variant: $experimentVariant and action is $action, the modal link is shown: $showModal',
- ({ action, experimentVariant, showModal }) => {
- stubExperiments({ invite_for_help_continuous_onboarding: experimentVariant });
- createWrapper(action);
-
- expect(openInviteMembesrModalLink().exists()).toBe(showModal);
- },
- );
- });
-
- describe('clicking the link to open the invite_members modal', () => {
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit').mockImplementation();
-
- stubExperiments({ invite_for_help_continuous_onboarding: 'candidate' });
- createWrapper('userAdded');
- });
-
- it('calls the eventHub', () => {
- openInviteMembesrModalLink().vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('openModal', { source: 'learn_gitlab' });
- });
-
- it('tracks the click', async () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- triggerEvent(openInviteMembesrModalLink().element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
- label: 'invite_your_colleagues',
- property: 'Growth::Activation::Experiment::InviteForHelpContinuousOnboarding',
- });
-
- unmockTracking();
- });
- });
-
- describe('video_tutorials_continuous_onboarding experiment', () => {
- describe('when control', () => {
- beforeEach(() => {
- stubExperiments({ video_tutorials_continuous_onboarding: 'control' });
- createWrapper('codeOwnersEnabled');
- });
-
- it('renders no video link', () => {
- expect(videoTutorialLink().exists()).toBe(false);
- });
- });
-
- describe('when candidate', () => {
- beforeEach(() => {
- stubExperiments({ video_tutorials_continuous_onboarding: 'candidate' });
- createWrapper('codeOwnersEnabled');
- });
-
- it('renders video link with blank target', () => {
- const videoLinkElement = videoTutorialLink();
-
- expect(videoLinkElement.exists()).toBe(true);
- expect(videoLinkElement.attributes('target')).toEqual('_blank');
- });
-
- it('tracks the click', () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- videoTutorialLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_video_link', {
- label: 'add_code_owners',
- property: 'Growth::Conversion::Experiment::LearnGitLab',
- context: {
- data: {
- experiment: 'video_tutorials_continuous_onboarding',
- variant: 'candidate',
- },
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
- },
- });
-
- unmockTracking();
- });
- });
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
deleted file mode 100644
index 0f63c243342..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ /dev/null
@@ -1,113 +0,0 @@
-import { GlProgressBar, GlAlert } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Cookies from '~/lib/utils/cookies';
-import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
-import eventHub from '~/invite_members/event_hub';
-import { INVITE_MODAL_OPEN_COOKIE } from '~/pages/projects/learn_gitlab/constants';
-import { testActions, testSections, testProject } from './mock_data';
-
-describe('Learn GitLab', () => {
- let wrapper;
- let sidebar;
-
- const createWrapper = () => {
- wrapper = mount(LearnGitlab, {
- propsData: {
- actions: testActions,
- sections: testSections,
- project: testProject,
- },
- });
- };
-
- beforeEach(() => {
- sidebar = document.createElement('div');
- sidebar.innerHTML = `
- <div class="sidebar-top-level-items">
- <div class="active">
- <div class="count"></div>
- </div>
- </div>
- `;
- document.body.appendChild(sidebar);
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- sidebar.remove();
- });
-
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders the progress percentage', () => {
- const text = wrapper.find('[data-testid="completion-percentage"]').text();
-
- expect(text).toBe('22% completed');
- });
-
- it('renders the progress bar with correct values', () => {
- const progressBar = wrapper.findComponent(GlProgressBar);
-
- expect(progressBar.attributes('value')).toBe('2');
- expect(progressBar.attributes('max')).toBe('9');
- });
-
- describe('Invite Members Modal', () => {
- let spy;
- let cookieSpy;
-
- beforeEach(() => {
- spy = jest.spyOn(eventHub, '$emit');
- cookieSpy = jest.spyOn(Cookies, 'remove');
- });
-
- afterEach(() => {
- Cookies.remove(INVITE_MODAL_OPEN_COOKIE);
- });
-
- it('emits openModal', () => {
- Cookies.set(INVITE_MODAL_OPEN_COOKIE, true);
-
- createWrapper();
-
- expect(spy).toHaveBeenCalledWith('openModal', {
- mode: 'celebrate',
- source: 'learn-gitlab',
- });
- expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
- });
-
- it('does not emit openModal when cookie is not set', () => {
- createWrapper();
-
- expect(spy).not.toHaveBeenCalled();
- expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
- });
- });
-
- describe('when the showSuccessfulInvitationsAlert event is fired', () => {
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- beforeEach(() => {
- eventHub.$emit('showSuccessfulInvitationsAlert');
- });
-
- it('displays the successful invitations alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('displays a message with the project name', () => {
- expect(findAlert().text()).toBe(
- "Your team is growing! You've successfully invited new team members to the test-project project.",
- );
- });
-
- it('modifies the sidebar percentage', () => {
- expect(sidebar.textContent.trim()).toBe('22%');
- });
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js
deleted file mode 100644
index 6ab57e31fed..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IncludedInTrialIndicator from '~/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue';
-
-describe('Learn GitLab Trial Card', () => {
- it('renders correctly', () => {
- const wrapper = shallowMount(IncludedInTrialIndicator);
-
- expect(wrapper.text()).toEqual('- Included in trial');
-
- wrapper.destroy();
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
deleted file mode 100644
index 1c29c68d2a9..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
+++ /dev/null
@@ -1,73 +0,0 @@
-export const testActions = {
- gitWrite: {
- url: 'http://example.com/',
- completed: true,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- userAdded: {
- url: 'http://example.com/',
- completed: true,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- pipelineCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- trialStarted: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- codeOwnersEnabled: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- requiredMrApprovalsEnabled: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- mergeRequestCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- securityScanEnabled: {
- url: 'https://docs.gitlab.com/ee/foobar/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- openInNewTab: true,
- },
- issueCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
-};
-
-export const testSections = {
- workspace: {
- svg: 'workspace.svg',
- },
- deploy: {
- svg: 'deploy.svg',
- },
- plan: {
- svg: 'plan.svg',
- },
-};
-
-export const testProject = {
- name: 'test-project',
-};
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 38f7a2e919d..ff20b72c72c 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
@@ -75,10 +75,7 @@ describe('Settings Panel', () => {
return mountFn(settingsPanel, {
propsData,
provide: {
- glFeatures: {
- packageRegistryAccessLevel: false,
- ...glFeatures,
- },
+ glFeatures,
},
stubs,
});
@@ -110,10 +107,8 @@ describe('Settings Panel', () => {
findContainerRegistrySettings().findComponent(GlSprintf);
const findContainerRegistryAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]');
- const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' });
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
- const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
const findPackageRegistryEnabledInput = () => wrapper.find('[name="package_registry_enabled"]');
const findPackageRegistryAccessLevelHiddenInput = () =>
wrapper.find(
@@ -512,180 +507,108 @@ describe('Settings Panel', () => {
});
describe('Packages', () => {
- it('should show the packages settings if packages are available', () => {
- wrapper = mountComponent({ packagesAvailable: true });
-
- expect(findPackageSettings().exists()).toBe(true);
- });
-
- it('should hide the packages settings if packages are not available', () => {
- wrapper = mountComponent({ packagesAvailable: false });
+ it('should hide the package access level settings with packagesAvailable = false', () => {
+ wrapper = mountComponent();
- expect(findPackageSettings().exists()).toBe(false);
+ expect(findPackageAccessLevel().exists()).toBe(false);
});
- it('should set the package settings help path', () => {
+ it('renders the package access level settings with packagesAvailable = true', () => {
wrapper = mountComponent({ packagesAvailable: true });
- expect(findPackageSettings().props('helpPath')).toBe(defaultProps.packagesHelpPath);
+ expect(findPackageAccessLevel().exists()).toBe(true);
});
- it('should enable the packages input when the repository is enabled', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().props('disabled')).toBe(false);
- });
-
- it('should disable the packages input when the repository is disabled', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().props('disabled')).toBe(true);
- });
-
- it('has label for toggle', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().findComponent(GlToggle).props('label')).toBe(
- settingsPanel.i18n.packagesLabel,
- );
- });
-
- it('should hide the package access level settings', () => {
- wrapper = mountComponent();
+ it('has hidden input field for package registry access level', () => {
+ wrapper = mountComponent({ packagesAvailable: true });
- expect(findPackageAccessLevel().exists()).toBe(false);
+ expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
});
- describe('packageRegistryAccessLevel feature flag = true', () => {
- it('should hide the packages settings', () => {
+ it.each`
+ 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}
+ `(
+ 'sets correct access level',
+ async ({
+ projectVisibilityLevel,
+ packageRegistryEnabled,
+ packageRegistryApiForEveryoneEnabled,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel: projectVisibilityLevel,
+ },
});
- expect(findPackageSettings().exists()).toBe(false);
- });
-
- it('should hide the package access level settings with packagesAvailable = false', () => {
- wrapper = mountComponent({ glFeatures: { packageRegistryAccessLevel: true } });
+ await findPackageRegistryEnabledInput().vm.$emit('change', packageRegistryEnabled);
- expect(findPackageAccessLevel().exists()).toBe(false);
- });
+ const packageRegistryApiForEveryoneEnabledInput = findPackageRegistryApiForEveryoneEnabledInput();
- it('renders the package access level settings with packagesAvailable = true', () => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- });
+ 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(findPackageAccessLevel().exists()).toBe(true);
- });
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
+ },
+ );
- it('has hidden input field for package registry access level', () => {
+ it.each`
+ 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}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${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 access level when project visibility level changed',
+ async ({
+ initialProjectVisibilityLevel,
+ newProjectVisibilityLevel,
+ initialAccessLevel,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel: initialProjectVisibilityLevel,
+ packageRegistryAccessLevel: initialAccessLevel,
+ },
});
- expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
- });
-
- it.each`
- 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}
- `(
- 'sets correct access level',
- async ({
- projectVisibilityLevel,
- packageRegistryEnabled,
- packageRegistryApiForEveryoneEnabled,
- expectedAccessLevel,
- }) => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- currentSettings: {
- visibilityLevel: projectVisibilityLevel,
- },
- });
-
- 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);
- },
- );
+ await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
- it.each`
- 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}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${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 access level when project visibility level changed',
- async ({
- initialProjectVisibilityLevel,
- newProjectVisibilityLevel,
- initialAccessLevel,
- expectedAccessLevel,
- }) => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- currentSettings: {
- visibilityLevel: initialProjectVisibilityLevel,
- packageRegistryAccessLevel: initialAccessLevel,
- },
- });
-
- await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
-
- expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
- },
- );
- });
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
+ },
+ );
});
describe('Pages', () => {
diff --git a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
deleted file mode 100644
index ce456d6c899..00000000000
--- a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = `
-"<div class=\\"badge\\">22</div>
-<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=issues\\">4</div>
-<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=merge_requests\\">5</div>"
-`;
diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js
deleted file mode 100644
index 6f14f0c70bd..00000000000
--- a/spec/frontend/pages/search/show/refresh_counts_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import refreshCounts from '~/pages/search/show/refresh_counts';
-
-const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`;
-const urlWithScope = (scope) => `${URL}&scope=${scope}`;
-const counts = [
- { scope: 'issues', count: 4 },
- { scope: 'merge_requests', count: 5 },
-];
-const fixture = `<div class="badge">22</div>
-<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div>
-<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`;
-
-describe('pages/search/show/refresh_counts', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- setHTMLFixture(fixture);
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('fetches and displays search counts', () => {
- counts.forEach(({ scope, count }) => {
- mock.onGet(urlWithScope(scope)).reply(200, { count });
- });
-
- // assert before act behavior
- return refreshCounts().then(() => {
- expect(document.body.innerHTML).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 67d0fbdd9d1..ffcfd1d9f78 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -110,17 +110,22 @@ describe('WikiForm', () => {
it('displays markdown editor', () => {
createWrapper({ persisted: true });
- expect(findMarkdownEditor().props()).toEqual(
+ const markdownEditor = findMarkdownEditor();
+
+ expect(markdownEditor.props()).toEqual(
expect.objectContaining({
value: pageInfoPersisted.content,
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
autofocus: pageInfoPersisted.persisted,
- formFieldId: 'wiki_content',
- formFieldName: 'wiki[content]',
}),
);
+
+ expect(markdownEditor.props('formFieldProps')).toMatchObject({
+ id: 'wiki_content',
+ name: 'wiki[content]',
+ });
});
it.each`
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 2da176dbfe4..f09b0cc3df8 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/performance_bar/components/performance_bar_app.vue';
import performanceBar from '~/performance_bar';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
@@ -26,7 +27,7 @@ describe('performance bar wrapper', () => {
mock = new MockAdapter(axios);
mock.onGet('/-/peek/results').reply(
- 200,
+ HTTP_STATUS_OK,
{
data: {
gc: {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index c9574208900..6519989661f 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -3,6 +3,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PersistentUserCallout from '~/persistent_user_callout';
jest.mock('~/flash');
@@ -88,7 +89,7 @@ describe('PersistentUserCallout', () => {
${'primary'}
${'secondary'}
`('POSTs endpoint and removes container when clicking $button close', async ({ button }) => {
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
buttons[button].click();
@@ -101,7 +102,7 @@ describe('PersistentUserCallout', () => {
});
it('invokes Flash when the dismiss request fails', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(500);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
buttons.primary.click();
@@ -140,7 +141,7 @@ describe('PersistentUserCallout', () => {
it('defers loading of a link until callout is dismissed', async () => {
const { href, target } = deferredLink;
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
deferredLink.click();
@@ -161,7 +162,7 @@ describe('PersistentUserCallout', () => {
});
it('does not follow link when notification is closed', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
button.click();
@@ -195,7 +196,7 @@ describe('PersistentUserCallout', () => {
it('uses a link to trigger callout and defers following until callout is finished', async () => {
const { href } = link;
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
link.click();
@@ -207,7 +208,7 @@ describe('PersistentUserCallout', () => {
});
it('invokes Flash when the dismiss request fails', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(500);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
link.click();
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 b7a9297d856..ab2056b4035 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
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
@@ -72,7 +73,7 @@ describe('Pipelines stage component', () => {
beforeEach(async () => {
createComponent({ updateDropdown: true });
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
await openStageDropdown();
});
@@ -121,7 +122,7 @@ describe('Pipelines stage component', () => {
describe('when user opens dropdown and stage request is successful', () => {
beforeEach(async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent();
await openStageDropdown();
@@ -148,7 +149,7 @@ describe('Pipelines stage component', () => {
describe('when user opens dropdown and stage request fails', () => {
it('should close the dropdown', async () => {
- mock.onGet(dropdownPath).reply(500);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await openStageDropdown();
@@ -163,7 +164,7 @@ describe('Pipelines stage component', () => {
beforeEach(async () => {
const copyStage = { ...stageReply };
copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
+ mock.onGet('bar.json').reply(HTTP_STATUS_OK, copyStage);
createComponent({
stage: {
status: {
@@ -188,8 +189,8 @@ describe('Pipelines stage component', () => {
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);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(HTTP_STATUS_OK);
createComponent();
await waitForPromises();
@@ -214,7 +215,7 @@ describe('Pipelines stage component', () => {
describe('With merge trains enabled', () => {
it('shows a warning on the dropdown', async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent({
isMergeTrain: true,
});
@@ -231,7 +232,7 @@ describe('Pipelines stage component', () => {
describe('With merge trains disabled', () => {
beforeEach(async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent();
await openStageDropdown();
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index a823e029281..e3eea503b46 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
describe('pipeline graph action component', () => {
@@ -12,18 +13,22 @@ describe('pipeline graph action component', () => {
const findButton = () => wrapper.findComponent(GlButton);
const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- mock.onPost('foo.json').reply(200);
+ const defaultProps = {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionIcon: 'cancel',
+ };
+ const createComponent = ({ props } = {}) => {
wrapper = mount(ActionComponent, {
- propsData: {
- tooltipText: 'bar',
- link: 'foo',
- actionIcon: 'cancel',
- },
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(HTTP_STATUS_OK);
});
afterEach(() => {
@@ -31,31 +36,39 @@ describe('pipeline graph action component', () => {
wrapper.destroy();
});
- it('should render the provided title as a bootstrap tooltip', () => {
- expect(findTooltipWrapper().attributes('title')).toBe('bar');
- });
+ describe('render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('should update bootstrap tooltip when title changes', async () => {
- wrapper.setProps({ tooltipText: 'changed' });
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(findTooltipWrapper().attributes('title')).toBe('bar');
+ });
- await nextTick();
- expect(findTooltipWrapper().attributes('title')).toBe('changed');
- });
+ it('should update bootstrap tooltip when title changes', async () => {
+ wrapper.setProps({ tooltipText: 'changed' });
- it('should render an svg', () => {
- expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
- expect(wrapper.find('svg').exists()).toBe(true);
+ await nextTick();
+ expect(findTooltipWrapper().attributes('title')).toBe('changed');
+ });
+
+ it('should render an svg', () => {
+ expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
+ expect(wrapper.find('svg').exists()).toBe(true);
+ });
});
describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successful request', async () => {
- jest.spyOn(wrapper.vm, '$emit');
+ beforeEach(() => {
+ createComponent();
+ });
+ it('emits `pipelineActionRequestComplete` after a successful request', async () => {
findButton().trigger('click');
await waitForPromises();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ expect(wrapper.emitted().pipelineActionRequestComplete).toHaveLength(1);
});
it('renders a loading icon while waiting for request', async () => {
@@ -65,4 +78,40 @@ describe('pipeline graph action component', () => {
expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
});
});
+
+ describe('when has a confirmation modal', () => {
+ beforeEach(() => {
+ createComponent({ props: { withConfirmationModal: true, shouldTriggerClick: false } });
+ });
+
+ describe('and a first click is initiated', () => {
+ beforeEach(async () => {
+ findButton().trigger('click');
+
+ await waitForPromises();
+ });
+
+ it('emits `showActionConfirmationModal` event', () => {
+ expect(wrapper.emitted().showActionConfirmationModal).toHaveLength(1);
+ });
+
+ it('does not emit `pipelineActionRequestComplete` event', () => {
+ expect(wrapper.emitted().pipelineActionRequestComplete).toBeUndefined();
+ });
+ });
+
+ describe('and the `shouldTriggerClick` value becomes true', () => {
+ beforeEach(async () => {
+ await wrapper.setProps({ shouldTriggerClick: true });
+ });
+
+ it('does not emit `showActionConfirmationModal` event', () => {
+ expect(wrapper.emitted().showActionConfirmationModal).toBeUndefined();
+ });
+
+ it('emits `actionButtonClicked` event', () => {
+ expect(wrapper.emitted().actionButtonClicked).toHaveLength(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 2abb5f7dc58..95207fd59ff 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,4 +1,5 @@
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
@@ -15,10 +16,11 @@ import {
describe('graph component', () => {
let wrapper;
+ const findDownstreamColumn = () => wrapper.findByTestId('downstream-pipelines');
const findLinkedColumns = () => wrapper.findAllComponents(LinkedPipelinesColumn);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
- const findStageNameInJob = () => wrapper.find('[data-testid="stage-name-in-job"]');
+ const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -64,14 +66,9 @@ describe('graph component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with data', () => {
beforeEach(() => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
});
it('renders the main columns in the graph', () => {
@@ -96,10 +93,20 @@ describe('graph component', () => {
});
});
+ describe('when column request an update to the retry confirmation modal', () => {
+ beforeEach(() => {
+ findStageColumns().at(0).vm.$emit('setSkipRetryModal');
+ });
+
+ it('setSkipRetryModal is emitted', () => {
+ expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1);
+ });
+ });
+
describe('when links are present', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
stubOverride: { 'job-item': false },
data: { hoveredJobName: 'test_a' },
});
@@ -116,7 +123,7 @@ describe('graph component', () => {
describe('when linked pipelines are not present', () => {
beforeEach(() => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
});
it('should not render a linked pipelines column', () => {
@@ -127,7 +134,7 @@ describe('graph component', () => {
describe('when linked pipelines are present', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) },
});
});
@@ -140,7 +147,7 @@ describe('graph component', () => {
describe('in layers mode', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
stubOverride: {
'job-item': false,
'job-group-dropdown': false,
@@ -156,4 +163,22 @@ describe('graph component', () => {
expect(findStageNameInJob().exists()).toBe(true);
});
});
+
+ describe('downstream pipelines', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse),
+ },
+ });
+ });
+
+ it('filters pipelines spawned from the same trigger job', () => {
+ // The mock data has one downstream with `retried: true and one
+ // with retried false. We filter the `retried: true` out so we
+ // should only pass one downstream
+ expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 587a3c67168..99bccd21656 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -10,6 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
@@ -199,6 +200,22 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('events', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+ describe('when receiving `setSkipRetryModal` event', () => {
+ it('passes down `skipRetryModal` value as true', async () => {
+ expect(getGraph().props('skipRetryModal')).toBe(false);
+
+ await getGraph().vm.$emit('setSkipRetryModal');
+
+ expect(getGraph().props('skipRetryModal')).toBe(true);
+ });
+ });
+ });
+
describe('when there is an error with an action in the graph', () => {
beforeEach(async () => {
createComponentWithApollo();
@@ -530,7 +547,7 @@ describe('Pipeline graph wrapper', () => {
describe('with duration and no error', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onPost(metricsPath).reply(200, {});
+ mock.onPost(metricsPath).reply(HTTP_STATUS_OK, {});
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [{ duration }];
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 05776ec0706..3224c87ab6b 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,44 +1,78 @@
+import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlModal } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/job_item.vue';
+import axios from '~/lib/utils/axios_utils';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
delayedJob,
mockJob,
mockJobWithoutDetails,
mockJobWithUnauthorizedAction,
+ mockFailedJob,
triggerJob,
+ triggerJobWithRetryAction,
} from './mock_data';
describe('pipeline graph job item', () => {
+ useLocalStorageSpy();
+
let wrapper;
+ let mockAxios;
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
const findActionComponent = () => wrapper.findByTestId('ci-action-component');
const findBadge = () => wrapper.findComponent(GlBadge);
+ const findJobLink = () => wrapper.findByTestId('job-with-link');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
+ const clickOnModalCancelBtn = () => findModal().vm.$emit('hide');
+ const clickOnModalCloseBtn = () => findModal().vm.$emit('close');
+
+ const myCustomClass1 = 'my-class-1';
+ const myCustomClass2 = 'my-class-2';
- const createWrapper = (propsData) => {
+ const defaultProps = {
+ job: mockJob,
+ };
+
+ const createWrapper = ({ props, data } = {}) => {
wrapper = extendedWrapper(
mount(JobItem, {
- propsData,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
}),
);
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
afterEach(() => {
- wrapper.destroy();
+ mockAxios.restore();
});
describe('name with link', () => {
it('should render the job name and status with a link', async () => {
- createWrapper({ job: mockJob });
+ createWrapper();
await nextTick();
- const link = wrapper.find('a');
+ const link = findJobLink();
expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
@@ -53,15 +87,17 @@ describe('pipeline graph job item', () => {
describe('name without link', () => {
beforeEach(() => {
createWrapper({
- job: mockJobWithoutDetails,
- cssClassJobName: 'css-class-job-name',
- jobHovered: 'test',
+ props: {
+ job: mockJobWithoutDetails,
+ cssClassJobName: 'css-class-job-name',
+ jobHovered: 'test',
+ },
});
});
it('should render status and name', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
- expect(wrapper.find('a').exists()).toBe(false);
+ expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
});
@@ -73,7 +109,7 @@ describe('pipeline graph job item', () => {
describe('action icon', () => {
it('should render the action icon', () => {
- createWrapper({ job: mockJob });
+ createWrapper();
const actionComponent = findActionComponent();
@@ -83,7 +119,11 @@ describe('pipeline graph job item', () => {
});
it('should render disabled action icon when user cannot run the action', () => {
- createWrapper({ job: mockJobWithUnauthorizedAction });
+ createWrapper({
+ props: {
+ job: mockJobWithUnauthorizedAction,
+ },
+ });
const actionComponent = findActionComponent();
@@ -91,18 +131,32 @@ describe('pipeline graph job item', () => {
expect(actionComponent.props('actionIcon')).toBe('stop');
expect(actionComponent.attributes('disabled')).toBe('disabled');
});
+
+ it('action icon tooltip text when job has passed but can be ran again', () => {
+ createWrapper({ props: { job: mockJob } });
+
+ expect(findActionComponent().props('tooltipText')).toBe('Run again');
+ });
+
+ it('action icon tooltip text when job has failed and can be retried', () => {
+ createWrapper({ props: { job: mockFailedJob } });
+
+ expect(findActionComponent().props('tooltipText')).toBe('Retry');
+ });
});
describe('job style', () => {
beforeEach(() => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'css-class-job-name',
+ props: {
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ },
});
});
it('should render provided class name', () => {
- expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ expect(findJobLink().classes()).toContain('css-class-job-name');
});
it('does not show a badge on the job item', () => {
@@ -117,11 +171,13 @@ describe('pipeline graph job item', () => {
describe('status label', () => {
it('should not render status label when it is not provided', () => {
createWrapper({
- job: {
- id: 4258,
- name: 'test',
- status: {
- icon: 'status_success',
+ props: {
+ job: {
+ id: 4258,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ },
},
},
});
@@ -131,13 +187,15 @@ describe('pipeline graph job item', () => {
it('should not render status label when it is provided', () => {
createWrapper({
- job: {
- id: 4259,
- name: 'test',
- status: {
- icon: 'status_success',
- label: 'success',
- tooltip: 'success',
+ props: {
+ job: {
+ id: 4259,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: 'success',
+ },
},
},
});
@@ -149,7 +207,9 @@ describe('pipeline graph job item', () => {
describe('for delayed job', () => {
it('displays remaining time in tooltip', () => {
createWrapper({
- job: delayedJob,
+ props: {
+ job: delayedJob,
+ },
});
expect(findJobWithLink().attributes('title')).toBe(
@@ -161,7 +221,11 @@ describe('pipeline graph job item', () => {
describe('trigger job', () => {
describe('card', () => {
beforeEach(() => {
- createWrapper({ job: triggerJob });
+ createWrapper({
+ props: {
+ job: triggerJob,
+ },
+ });
});
it('shows a badge on the job item', () => {
@@ -182,7 +246,12 @@ describe('pipeline graph job item', () => {
`(
`trigger job should stay highlighted when downstream is expanded`,
({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ createWrapper({
+ props: {
+ job,
+ pipelineExpanded: { jobName, expanded },
+ },
+ });
const findJobEl = link ? findJobWithLink : findJobWithoutLink;
expect(findJobEl().classes()).toContain(triggerActiveClass);
@@ -196,7 +265,12 @@ describe('pipeline graph job item', () => {
`(
`trigger job should not be highlighted when downstream is not expanded`,
({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ createWrapper({
+ props: {
+ job,
+ pipelineExpanded: { jobName, expanded },
+ },
+ });
const findJobEl = link ? findJobWithLink : findJobWithoutLink;
expect(findJobEl().classes()).not.toContain(triggerActiveClass);
@@ -208,60 +282,182 @@ describe('pipeline graph job item', () => {
describe('job classes', () => {
it('job class is shown', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'my-class',
+ props: {
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class');
+ const jobLinkEl = findJobLink();
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).toContain('my-class');
+
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('job class is shown, along with hover', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'my-class',
- sourceJobHovered: mockJob.name,
+ props: {
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ sourceJobHovered: mockJob.name,
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class');
- expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain('my-class');
+ expect(jobLinkEl.classes()).toContain(triggerActiveClass);
});
it('multiple job classes are shown', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: ['my-class-1', 'my-class-2'],
+ props: {
+ job: mockJob,
+ cssClassJobName: [myCustomClass1, myCustomClass2],
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('multiple job classes are shown conditionally', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: { 'my-class-1': true, 'my-class-2': true },
+ props: {
+ job: mockJob,
+ cssClassJobName: { [myCustomClass1]: true, [myCustomClass2]: true },
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('multiple job classes are shown, along with a hover', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: ['my-class-1', 'my-class-2'],
- sourceJobHovered: mockJob.name,
+ props: {
+ job: mockJob,
+ cssClassJobName: [myCustomClass1, myCustomClass2],
+ sourceJobHovered: mockJob.name,
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
- expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
+ expect(jobLinkEl.classes()).toContain(triggerActiveClass);
+ });
+ });
+
+ describe('confirmation modal', () => {
+ describe('when clicking on the action component', () => {
+ it.each`
+ skipRetryModal | exists | visibilityText
+ ${false} | ${true} | ${'shows'}
+ ${true} | ${false} | ${'hides'}
+ `(
+ '$visibilityText the modal when `skipRetryModal` is $skipRetryModal',
+ async ({ exists, skipRetryModal }) => {
+ createWrapper({
+ props: {
+ skipRetryModal,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+
+ expect(findModal().exists()).toBe(exists);
+ },
+ );
+ });
+
+ describe('when showing the modal', () => {
+ it.each`
+ buttonName | shouldTriggerActionClick | actionBtn
+ ${'primary'} | ${true} | ${clickOnModalPrimaryBtn}
+ ${'cancel'} | ${false} | ${clickOnModalCancelBtn}
+ ${'close'} | ${false} | ${clickOnModalCloseBtn}
+ `(
+ 'clicking on $buttonName will pass down shouldTriggerActionClick as $shouldTriggerActionClick to the action component',
+ async ({ shouldTriggerActionClick, actionBtn }) => {
+ createWrapper({
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+
+ await actionBtn();
+
+ expect(findActionComponent().props().shouldTriggerClick).toBe(shouldTriggerActionClick);
+ },
+ );
+ });
+
+ describe('when not checking the "do not show this again" checkbox', () => {
+ it.each`
+ actionName | actionBtn
+ ${'closing'} | ${clickOnModalCloseBtn}
+ ${'cancelling'} | ${clickOnModalCancelBtn}
+ ${'confirming'} | ${clickOnModalPrimaryBtn}
+ `(
+ 'does not emit any event and will not modify localstorage on $actionName',
+ async ({ actionBtn }) => {
+ createWrapper({
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+ await actionBtn();
+
+ expect(wrapper.emitted().setSkipRetryModal).toBeUndefined();
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ },
+ );
+ });
+
+ describe('when checking the "do not show this again" checkbox', () => {
+ it.each`
+ actionName | actionBtn
+ ${'closing'} | ${clickOnModalCloseBtn}
+ ${'cancelling'} | ${clickOnModalCancelBtn}
+ ${'confirming'} | ${clickOnModalPrimaryBtn}
+ `(
+ 'emits "setSkipRetryModal" and set local storage key on $actionName the modal',
+ async ({ actionBtn }) => {
+ // We are passing the checkbox as a slot to the GlModal.
+ // The way GlModal is mounted, we can neither click on the box
+ // or emit an event directly. We therefore set the data property
+ // as it would be if the box was checked.
+ createWrapper({
+ data: {
+ currentSkipModalValue: true,
+ },
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+ await actionBtn();
+
+ expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith('skip_retry_modal', 'true');
+ },
+ );
});
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 399d52c3dff..f396fe2aff4 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -5,10 +5,10 @@ import { mount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
-import { PIPELINE_GRAPHQL_TYPE } from '~/pipelines/constants';
import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
@@ -219,7 +219,7 @@ describe('Linked pipeline', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: RetryPipelineMutation,
variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
},
});
});
@@ -285,7 +285,7 @@ describe('Linked pipeline', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: CancelPipelineMutation,
variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
},
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 6124d67af09..08624cc511d 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,5 +1,9 @@
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
-import { BUILD_KIND, BRIDGE_KIND } from '~/pipelines/components/graph/constants';
+import {
+ BUILD_KIND,
+ BRIDGE_KIND,
+ RETRY_ACTION_TITLE,
+} from '~/pipelines/components/graph/constants';
export const mockPipelineResponse = {
data: {
@@ -726,6 +730,7 @@ export const downstream = {
sourceJob: {
name: 'test_c',
id: '71',
+ retried: false,
__typename: 'CiJob',
},
project: {
@@ -756,6 +761,7 @@ export const downstream = {
sourceJob: {
id: '73',
name: 'test_d',
+ retried: true,
__typename: 'CiJob',
},
project: {
@@ -841,6 +847,7 @@ export const wrappedPipelineReturn = {
sourceJob: {
name: 'test_c',
id: '78',
+ retried: false,
__typename: 'CiJob',
},
project: {
@@ -1038,3 +1045,38 @@ export const triggerJob = {
action: null,
},
};
+
+export const triggerJobWithRetryAction = {
+ ...triggerJob,
+ status: {
+ ...triggerJob.status,
+ action: {
+ icon: 'retry',
+ title: RETRY_ACTION_TITLE,
+ path: '/root/ci-mock/builds/4259/retry',
+ method: 'post',
+ },
+ },
+};
+
+export const mockFailedJob = {
+ id: 3999,
+ name: 'failed job',
+ kind: BUILD_KIND,
+ status: {
+ id: 'failed-3999-3999',
+ icon: 'status_failed',
+ tooltip: 'failed - (stuck or timeout failure)',
+ hasDetails: true,
+ detailsPath: '/root/ci-project/-/jobs/3999',
+ group: 'failed',
+ label: 'failed',
+ action: {
+ id: 'Ci::BuildPresenter-failed-3999',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/ci-project/-/jobs/3999/retry',
+ title: 'Retry',
+ },
+ },
+};
diff --git a/spec/frontend/pipelines/linked_pipelines_mock.json b/spec/frontend/pipelines/linked_pipelines_mock.json
index 8ad19ef4865..a68283032d2 100644
--- a/spec/frontend/pipelines/linked_pipelines_mock.json
+++ b/spec/frontend/pipelines/linked_pipelines_mock.json
@@ -7,7 +7,7 @@
"state": "active",
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
"web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\"\u003e🍕\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "status_tooltip_html": "<span class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"><gl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\">🍕</gl-emoji></span>",
"path": "/axil"
},
"active": false,
@@ -68,7 +68,7 @@
"title": "Play",
"path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -104,7 +104,7 @@
"title": "Play",
"path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -290,7 +290,9 @@
"dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "review-docs-cleanup",
@@ -305,7 +307,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"ref": {
"name": "docs/add-development-guide-to-readme",
@@ -319,7 +323,9 @@
"short_id": "8083eb0a",
"title": "Add link to development guide in readme",
"created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "parent_ids": [
+ "1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"
+ ],
"message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
"author_name": "Achilleas Pipinellis",
"author_email": "axil@gitlab.com",
@@ -337,7 +343,7 @@
"status_tooltip_html": null,
"path": "/axil"
},
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80&d=identicon",
"commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
"commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
},
@@ -402,7 +408,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -437,7 +443,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -466,7 +472,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -501,7 +507,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -530,7 +536,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -565,7 +571,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -782,7 +788,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -809,7 +817,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -875,7 +885,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -910,7 +920,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -939,7 +949,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -974,7 +984,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1003,7 +1013,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1038,7 +1048,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1255,7 +1265,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -1282,7 +1294,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -1291,7 +1305,9 @@
"full_name": "GitLab.com / GitLab Docs"
}
},
- "triggered": []
+ "triggered": [
+
+ ]
},
"triggered": [
{
@@ -1352,7 +1368,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1387,7 +1403,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1416,7 +1432,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1451,7 +1467,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1480,7 +1496,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1515,7 +1531,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1732,7 +1748,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -1759,7 +1777,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -1767,7 +1787,10 @@
"full_path": "/gitlab-com/gitlab-docs",
"full_name": "GitLab.com / GitLab Docs"
},
- "triggered": [{}]
+ "triggered": [
+ {
+ }
+ ]
},
{
"id": 34993052,
@@ -1827,7 +1850,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1862,7 +1885,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1891,7 +1914,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1926,7 +1949,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1955,7 +1978,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1990,7 +2013,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -2207,7 +2230,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -2234,7 +2259,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -3373,7 +3400,7 @@
"title": "Play",
"path": "/h5bp/html5-boilerplate/-/jobs/545/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -3409,7 +3436,7 @@
"title": "Play",
"path": "/h5bp/html5-boilerplate/-/jobs/545/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -3467,7 +3494,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"ref": {
"name": "master",
@@ -3481,7 +3510,9 @@
"short_id": "bad98c45",
"title": "remove instances of shrink-to-fit=no (#2103)",
"created_at": "2018-12-17T20:52:18.000Z",
- "parent_ids": ["49130f6cfe9ff1f749015d735649a2bc6f66cf3a"],
+ "parent_ids": [
+ "49130f6cfe9ff1f749015d735649a2bc6f66cf3a"
+ ],
"message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.",
"author_name": "Scott O'Hara",
"author_email": "scottaohara@users.noreply.github.com",
@@ -3490,7 +3521,7 @@
"committer_email": "rob@drunkenfist.com",
"committed_date": "2018-12-17T20:52:18.000Z",
"author": null,
- "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon",
+ "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80&d=identicon",
"commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b",
"commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b"
},
@@ -3522,7 +3553,9 @@
"full_name": "Gitlab Org / Gitlab Test"
}
},
- "triggered": [],
+ "triggered": [
+
+ ],
"project": {
"id": 1794617,
"name": "GitLab Docs",
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 36bce65dd56..dd7e81f3f22 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -837,7 +837,6 @@ export const mockPipelineTag = () => {
duration: 93,
finished_at: '2022-02-02T15:40:59.384Z',
event_type_name: 'Pipeline',
- name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
},
@@ -1045,7 +1044,6 @@ export const mockPipelineBranch = () => {
duration: 75,
finished_at: '2022-01-14T18:02:35.842Z',
event_type_name: 'Pipeline',
- name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
},
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index f0dae8ebcbe..bedde71c48d 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -5,6 +5,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineMultiActions, {
i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
@@ -79,7 +80,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
describe('Artifacts', () => {
it('should fetch artifacts and show search box on dropdown click', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
@@ -140,7 +141,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
describe('with a failing request', () => {
it('should render an error message', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(500);
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index a70ef10aa7b..e034d52a33c 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
@@ -70,7 +71,7 @@ describe('Pipelines Actions dropdown', () => {
describe('on click', () => {
it('makes a request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(200);
+ mock.onPost(mockActions.path).reply(HTTP_STATUS_OK);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -82,7 +83,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('makes a failed request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(500);
+ mock.onPost(mockActions.path).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -132,7 +133,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('makes post request after confirming', async () => {
- mock.onPost(scheduledJobAction.path).reply(200);
+ mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(true);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -145,7 +146,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('does not make post request if confirmation is cancelled', async () => {
- mock.onPost(scheduledJobAction.path).reply(200);
+ mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(false);
findAllDropdownItems().at(0).vm.$emit('click');
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 351572fc83a..2523b901506 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -13,6 +13,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { createAlert, VARIANT_WARNING } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
@@ -141,7 +142,7 @@ describe('Pipelines', () => {
beforeEach(() => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .reply(200, mockPipelinesResponse);
+ .reply(HTTP_STATUS_OK, mockPipelinesResponse);
});
describe('when user has no permissions', () => {
@@ -233,7 +234,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
- .reply(200, {
+ .reply(HTTP_STATUS_OK, {
pipelines: [mockFinishedPipeline],
count: mockPipelinesResponse.count,
});
@@ -277,7 +278,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
- .reply(200, {
+ .reply(HTTP_STATUS_OK, {
pipelines: [],
count: mockPipelinesResponse.count,
});
@@ -320,7 +321,7 @@ describe('Pipelines', () => {
.onGet(mockPipelinesEndpoint, {
params: expectedParams,
})
- .replyOnce(200, {
+ .replyOnce(HTTP_STATUS_OK, {
pipelines: [mockFilteredPipeline],
count: mockPipelinesResponse.count,
});
@@ -398,7 +399,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: firstPage,
count: mockPipelinesResponse.count,
@@ -406,7 +407,7 @@ describe('Pipelines', () => {
mockPageHeaders({ page: 1 }),
);
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: secondPage,
count: mockPipelinesResponse.count,
@@ -448,6 +449,26 @@ describe('Pipelines', () => {
`${window.location.pathname}?page=2&scope=all`,
);
});
+
+ it('should reset page to 1 when filtering pipelines', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=2&scope=all`,
+ );
+
+ findFilteredSearch().vm.$emit('submit', [
+ { type: 'status', value: { data: 'success', operator: '=' } },
+ ]);
+
+ expect(window.history.pushState).toHaveBeenCalledTimes(2);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all&status=success`,
+ );
+ });
});
});
@@ -461,13 +482,13 @@ describe('Pipelines', () => {
// Mock no pipelines in the first attempt
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .replyOnce(200, emptyResponse, {
+ .replyOnce(HTTP_STATUS_OK, emptyResponse, {
'POLL-INTERVAL': 100,
});
// Mock pipelines in the next attempt
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .reply(200, mockPipelinesResponse, {
+ .reply(HTTP_STATUS_OK, mockPipelinesResponse, {
'POLL-INTERVAL': 100,
});
});
@@ -508,10 +529,12 @@ describe('Pipelines', () => {
describe('when no pipelines exist', () => {
beforeEach(() => {
- mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
- pipelines: [],
- count: { all: '0' },
- });
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(HTTP_STATUS_OK, {
+ pipelines: [],
+ count: { all: '0' },
+ });
});
describe('when CI is enabled and user has permissions', () => {
@@ -550,10 +573,12 @@ describe('Pipelines', () => {
});
it('renders tab empty state finished scope', async () => {
- mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
- pipelines: [],
- count: { all: '0' },
- });
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
+ .reply(HTTP_STATUS_OK, {
+ pipelines: [],
+ count: { all: '0' },
+ });
findNavigationTabs().vm.$emit('onChangeTab', 'finished');
@@ -643,7 +668,7 @@ describe('Pipelines', () => {
beforeEach(() => {
mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: [mockPipelineWithStages],
count: { all: '1' },
@@ -653,7 +678,9 @@ describe('Pipelines', () => {
},
);
- mock.onGet(mockPipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply);
+ mock
+ .onGet(mockPipelineWithStages.details.stages[0].dropdown_path)
+ .reply(HTTP_STATUS_OK, stageReply);
createComponent();
@@ -664,7 +691,7 @@ describe('Pipelines', () => {
describe('when a request is being made', () => {
beforeEach(async () => {
- mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
+ mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_OK, mockPipelinesResponse);
await waitForPromises();
});
@@ -702,7 +729,7 @@ describe('Pipelines', () => {
describe('when pipelines cannot be loaded', () => {
beforeEach(async () => {
- mock.onGet(mockPipelinesEndpoint).reply(500, {});
+ mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
});
describe('when user has no permissions', () => {
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 9359bd9b95f..6ec8901038b 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -121,6 +121,14 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
+ it('should render the latest downstream pipelines only', () => {
+ // component receives two downstream pipelines. one of them is already outdated
+ // because we retried the trigger job, so the mini pipeline graph will only
+ // render the newly created downstream pipeline instead
+ expect(pipeline.triggered).toHaveLength(2);
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
+
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 6e61ef97257..f6287107ed0 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
@@ -35,7 +36,7 @@ describe('Actions TestReports Store', () => {
describe('fetch report summary', () => {
beforeEach(() => {
- mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
+ mock.onGet(summaryEndpoint).replyOnce(HTTP_STATUS_OK, summary, {});
});
it('sets testReports and shows tests', () => {
@@ -66,7 +67,7 @@ describe('Actions TestReports Store', () => {
testReports.test_suites[0].build_ids = buildIds;
mock
.onGet(suiteEndpoint, { params: { build_ids: buildIds } })
- .replyOnce(200, testReports.test_suites[0], {});
+ .replyOnce(HTTP_STATUS_OK, testReports.test_suites[0], {});
});
it('sets test suite and shows tests', () => {
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index 1c23a7e4fcf..51e0e0705ff 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -3,6 +3,7 @@ import {
makeLinksFromNodes,
filterByAncestors,
generateColumnsFromLayersListBare,
+ keepLatestDownstreamPipelines,
listByLayers,
parseData,
removeOrphanNodes,
@@ -10,6 +11,8 @@ import {
} from '~/pipelines/components/parsing_utils';
import { createNodeDict } from '~/pipelines/utils';
+import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
+import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
@@ -159,3 +162,37 @@ describe('DAG visualization parsing utilities', () => {
});
});
});
+
+describe('linked pipeline utilities', () => {
+ describe('keepLatestDownstreamPipelines', () => {
+ it('filters data from GraphQL', () => {
+ const downstream = mockDownstreamPipelinesGraphql().nodes;
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(downstream).toHaveLength(3);
+ expect(latestDownstream).toHaveLength(1);
+ });
+
+ it('filters data from REST', () => {
+ const downstream = mockDownstreamPipelinesRest();
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(downstream).toHaveLength(2);
+ expect(latestDownstream).toHaveLength(1);
+ });
+
+ it('returns downstream pipelines if sourceJob.retried is null', () => {
+ const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes;
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(latestDownstream).toHaveLength(downstream.length);
+ });
+
+ it('returns downstream pipelines if source_job.retried is null', () => {
+ const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false });
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(latestDownstream).toHaveLength(downstream.length);
+ });
+ });
+});
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 575df9fb3c0..fa0e86a7b05 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
jest.mock('~/flash');
@@ -97,7 +97,7 @@ describe('UpdateUsername component', () => {
});
it('executes API call on confirmation button click', async () => {
- axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
+ axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
await wrapper.vm.onConfirm();
@@ -114,7 +114,7 @@ describe('UpdateUsername component', () => {
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
- return [200, { message: 'Username changed' }];
+ return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
await wrapper.vm.onConfirm();
@@ -133,7 +133,7 @@ describe('UpdateUsername component', () => {
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
- return [400, { message: 'Invalid username' }];
+ return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
@@ -144,7 +144,7 @@ describe('UpdateUsername component', () => {
it('shows an error message if the error response has a `message` property', async () => {
axiosMock.onPut(actionUrl).replyOnce(() => {
- return [400, { message: 'Invalid username' }];
+ return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
@@ -156,7 +156,7 @@ describe('UpdateUsername component', () => {
it("shows a fallback error message if the error response doesn't have a `message` property", async () => {
axiosMock.onPut(actionUrl).replyOnce(() => {
- return [400];
+ return [HTTP_STATUS_BAD_REQUEST];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
diff --git a/spec/frontend/profile/components/activity_tab_spec.js b/spec/frontend/profile/components/activity_tab_spec.js
new file mode 100644
index 00000000000..9363aad70fd
--- /dev/null
+++ b/spec/frontend/profile/components/activity_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import ActivityTab from '~/profile/components/activity_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ActivityTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ActivityTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Activity'));
+ });
+});
diff --git a/spec/frontend/profile/components/contributed_projects_tab_spec.js b/spec/frontend/profile/components/contributed_projects_tab_spec.js
new file mode 100644
index 00000000000..1ee55dc033d
--- /dev/null
+++ b/spec/frontend/profile/components/contributed_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ContributedProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributedProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Contributed projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
new file mode 100644
index 00000000000..4af428c4e0c
--- /dev/null
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import FollowersTab from '~/profile/components/followers_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FollowersTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(FollowersTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers'));
+ });
+});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
new file mode 100644
index 00000000000..75123274ccb
--- /dev/null
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import FollowingTab from '~/profile/components/following_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FollowingTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(FollowingTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following'));
+ });
+});
diff --git a/spec/frontend/profile/components/groups_tab_spec.js b/spec/frontend/profile/components/groups_tab_spec.js
new file mode 100644
index 00000000000..ec480924bdb
--- /dev/null
+++ b/spec/frontend/profile/components/groups_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import GroupsTab from '~/profile/components/groups_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('GroupsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Groups'));
+ });
+});
diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js
new file mode 100644
index 00000000000..eb27515bca3
--- /dev/null
+++ b/spec/frontend/profile/components/overview_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import OverviewTab from '~/profile/components/overview_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('OverviewTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(OverviewTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview'));
+ });
+});
diff --git a/spec/frontend/profile/components/personal_projects_tab_spec.js b/spec/frontend/profile/components/personal_projects_tab_spec.js
new file mode 100644
index 00000000000..a701856c544
--- /dev/null
+++ b/spec/frontend/profile/components/personal_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('PersonalProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(PersonalProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Personal projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
new file mode 100644
index 00000000000..11ab372f1dd
--- /dev/null
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -0,0 +1,36 @@
+import ProfileTabs from '~/profile/components/profile_tabs.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import OverviewTab from '~/profile/components/overview_tab.vue';
+import ActivityTab from '~/profile/components/activity_tab.vue';
+import GroupsTab from '~/profile/components/groups_tab.vue';
+import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
+import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
+import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
+import SnippetsTab from '~/profile/components/snippets_tab.vue';
+import FollowersTab from '~/profile/components/followers_tab.vue';
+import FollowingTab from '~/profile/components/following_tab.vue';
+
+describe('ProfileTabs', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProfileTabs);
+ };
+
+ it.each([
+ OverviewTab,
+ ActivityTab,
+ GroupsTab,
+ ContributedProjectsTab,
+ PersonalProjectsTab,
+ StarredProjectsTab,
+ SnippetsTab,
+ FollowersTab,
+ FollowingTab,
+ ])('renders $i18n.title tab', (tab) => {
+ createComponent();
+
+ expect(wrapper.findComponent(tab).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/profile/components/snippets_tab_spec.js b/spec/frontend/profile/components/snippets_tab_spec.js
new file mode 100644
index 00000000000..1306757314c
--- /dev/null
+++ b/spec/frontend/profile/components/snippets_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import SnippetsTab from '~/profile/components/snippets_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('SnippetsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SnippetsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets'));
+ });
+});
diff --git a/spec/frontend/profile/components/starred_projects_tab_spec.js b/spec/frontend/profile/components/starred_projects_tab_spec.js
new file mode 100644
index 00000000000..b9f2839172f
--- /dev/null
+++ b/spec/frontend/profile/components/starred_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('StarredProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(StarredProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Starred projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
index 3025a2f87ae..f675b6cf15c 100644
--- a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
+++ b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
@@ -396,7 +396,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -447,7 +446,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -620,7 +618,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -676,7 +673,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -736,7 +732,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -782,7 +777,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -832,7 +826,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -878,7 +871,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
deleted file mode 100644
index b8d5a1a61f3..00000000000
--- a/spec/frontend/project_select_combo_button_spec.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import ProjectSelectComboButton from '~/project_select_combo_button';
-
-const fixturePath = 'static/project_select_combo_button.html';
-
-describe('Project Select Combo Button', () => {
- let testContext;
-
- beforeEach(() => {
- testContext = {};
- });
-
- beforeEach(() => {
- testContext.defaults = {
- label: 'Select project to create issue',
- groupId: 12345,
- projectMeta: {
- name: 'My Cool Project',
- url: 'http://mycoolproject.com',
- },
- newProjectMeta: {
- name: 'My Other Cool Project',
- url: 'http://myothercoolproject.com',
- },
- vulnerableProject: {
- name: 'Self XSS',
- // eslint-disable-next-line no-script-url
- url: 'javascript:alert(1)',
- },
- localStorageKey: 'group-12345-new-issue-recent-project',
- relativePath: 'issues/new',
- };
-
- loadHTMLFixture(fixturePath);
-
- testContext.newItemBtn = document.querySelector('.js-new-project-item-link');
- testContext.projectSelectInput = document.querySelector('.project-item-select');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('on page load when localStorage is empty', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
- });
-
- it('newItemBtn href is null', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe('');
- });
-
- it('newItemBtn text is the plain default label', () => {
- expect(testContext.newItemBtn.textContent).toBe(testContext.defaults.label);
- });
- });
-
- describe('on page load when localStorage is filled', () => {
- beforeEach(() => {
- window.localStorage.setItem(
- testContext.defaults.localStorageKey,
- JSON.stringify(testContext.defaults.projectMeta),
- );
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
- });
-
- it('newItemBtn href is correctly set', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe(
- testContext.defaults.projectMeta.url,
- );
- });
-
- it('newItemBtn text is the cached label', () => {
- expect(testContext.newItemBtn.textContent).toBe(
- `New issue in ${testContext.defaults.projectMeta.name}`,
- );
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('after selecting a new project', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- // mock the effect of selecting an item from the projects dropdown (select2)
- $('.project-item-select')
- .val(JSON.stringify(testContext.defaults.newProjectMeta))
- .trigger('change');
- });
-
- it('newItemBtn href is correctly set', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe(
- 'http://myothercoolproject.com/issues/new',
- );
- });
-
- it('newItemBtn text is the selected project label', () => {
- expect(testContext.newItemBtn.textContent).toBe(
- `New issue in ${testContext.defaults.newProjectMeta.name}`,
- );
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('after selecting a vulnerable project', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- // mock the effect of selecting an item from the projects dropdown (select2)
- $('.project-item-select')
- .val(JSON.stringify(testContext.defaults.vulnerableProject))
- .trigger('change');
- });
-
- it('newItemBtn href is correctly sanitized', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe('about:blank');
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('deriveTextVariants', () => {
- beforeEach(() => {
- testContext.mockExecutionContext = {
- resourceType: '',
- resourceLabel: '',
- };
-
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- testContext.method = testContext.comboButton.deriveTextVariants.bind(
- testContext.mockExecutionContext,
- );
- });
-
- it('correctly derives test variants for merge requests', () => {
- testContext.mockExecutionContext.resourceType = 'merge_requests';
- testContext.mockExecutionContext.resourceLabel = 'New merge request';
-
- const returnedVariants = testContext.method();
-
- expect(returnedVariants.localStorageItemType).toBe('new-merge-request');
- expect(returnedVariants.presetTextSuffix).toBe('merge request');
- });
-
- it('correctly derives text variants for issues', () => {
- testContext.mockExecutionContext.resourceType = 'issues';
- testContext.mockExecutionContext.resourceLabel = 'New issue';
-
- const returnedVariants = testContext.method();
-
- expect(returnedVariants.localStorageItemType).toBe('new-issue');
- expect(returnedVariants.presetTextSuffix).toBe('issue');
- });
- });
-});
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index a84dd246f5d..6aa5a9a5a3a 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,9 +1,8 @@
-import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
@@ -35,11 +34,11 @@ describe('BranchesDropdown', () => {
);
};
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findNoResults = () => wrapper.findByTestId('empty-result-message');
- const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ beforeEach(() => {
+ createComponent({ value: '' });
+ });
afterEach(() => {
wrapper.destroy();
@@ -48,138 +47,36 @@ describe('BranchesDropdown', () => {
});
describe('On mount', () => {
- beforeEach(() => {
- 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({ value: '' }, { isFetching: true });
+ describe('Value prop changes in parent component', () => {
+ it('triggers fetchBranches call', async () => {
+ await wrapper.setProps({ value: 'new value' });
- expect(findLoading().isVisible()).toBe(true);
- });
-
- it('does not show loading icon', () => {
- createComponent({ value: '' });
-
- expect(findLoading().isVisible()).toBe(false);
- });
- });
-
- describe('No branches found', () => {
- beforeEach(() => {
- createComponent({ value: '_non_existent_branch_' });
- });
-
- it('renders empty results message', () => {
- expect(findNoResults().text()).toBe('No matching results');
- });
-
- it('shows GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search branches',
- debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
- });
+ expect(spyFetchBranches).toHaveBeenCalled();
});
});
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
+ describe('Selecting Dropdown Item', () => {
+ it('emits event', async () => {
+ findDropdown().vm.$emit('select', '_anything_');
- it('renders all branches when search term is empty', () => {
- 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('should not be selected on the inactive branch', () => {
- expect(wrapper.vm.isSelected('_main_')).toBe(false);
+ expect(wrapper.emitted()).toHaveProperty('input');
});
});
describe('When searching', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
-
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
- findSearchBoxByType().vm.$emit('input', '_anything_');
+ findDropdown().vm.$emit('search', '_anything_');
await nextTick();
expect(spy).toHaveBeenCalledWith('_anything_');
- expect(wrapper.vm.searchTerm).toBe('_anything_');
- });
- });
-
- describe('Branches found', () => {
- beforeEach(() => {
- createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
- });
-
- it('renders only the branch searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
- });
-
- it('should not display empty results message', () => {
- expect(findNoResults().exists()).toBe(false);
- });
-
- it('should signify this branch is selected', () => {
- expect(wrapper.vm.isSelected('_branch_1_')).toBe(true);
- });
-
- it('should signify the branch is not selected', () => {
- expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false);
- });
-
- describe('Custom events', () => {
- it('should emit selectBranch if an branch is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
-
- expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]);
- expect(wrapper.vm.searchTerm).toBe('_branch_1_');
- });
- });
- });
-
- describe('Case insensitive for search term', () => {
- beforeEach(() => {
- createComponent({ value: '_BrAnCh_1_' });
- });
-
- it('renders only the branch searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
});
});
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 20c312ec771..c59cf700e0d 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -157,7 +157,7 @@ describe('CommitFormModal', () => {
});
it('Changes the start_branch input value', async () => {
- findBranchesDropdown().vm.$emit('selectBranch', '_changed_branch_value_');
+ findBranchesDropdown().vm.$emit('input', '_changed_branch_value_');
await nextTick();
diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
index bb20918e0cd..0e213ff388a 100644
--- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -35,78 +35,23 @@ describe('ProjectsDropdown', () => {
);
};
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findNoResults = () => wrapper.findByTestId('empty-result-message');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
afterEach(() => {
wrapper.destroy();
spyFetchProjects.mockReset();
});
- describe('No projects found', () => {
- beforeEach(() => {
- createComponent('_non_existent_project_');
- });
-
- it('renders empty results message', () => {
- expect(findNoResults().text()).toBe('No matching results');
- });
-
- it('shows GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search projects',
- });
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
- });
-
- it('renders all projects when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
- expect(findDropdownItemByIndex(1).text()).toBe('_project_2_');
- expect(findDropdownItemByIndex(2).text()).toBe('_project_3_');
- });
-
- it('should not be selected on the inactive project', () => {
- expect(wrapper.vm.isSelected('_project_1_')).toBe(false);
- });
- });
-
describe('Projects found', () => {
beforeEach(() => {
createComponent('_project_1_', { targetProjectId: '1' });
});
- it('renders only the project searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
- });
-
- it('should not display empty results message', () => {
- expect(findNoResults().exists()).toBe(false);
- });
-
- it('should signify this project is selected', () => {
- expect(findDropdownItemByIndex(0).props('isChecked')).toBe(true);
- });
-
- it('should signify the project is not selected', () => {
- expect(wrapper.vm.isSelected('_not_selected_project_')).toBe(false);
- });
-
describe('Custom events', () => {
it('should emit selectProject if a project is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
+ findDropdown().vm.$emit('select', '1');
expect(wrapper.emitted('selectProject')).toEqual([['1']]);
- expect(wrapper.vm.filterTerm).toBe('_project_1_');
});
});
});
@@ -117,8 +62,7 @@ describe('ProjectsDropdown', () => {
});
it('renders only the project searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
+ expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
});
});
});
diff --git a/spec/frontend/projects/commit/mock_data.js b/spec/frontend/projects/commit/mock_data.js
index 34e9c400af4..e398d46e69c 100644
--- a/spec/frontend/projects/commit/mock_data.js
+++ b/spec/frontend/projects/commit/mock_data.js
@@ -24,5 +24,9 @@ export default {
openModal: '_open_modal_',
},
mockBranches: ['_branch_1', '_abc_', '_main_'],
- mockProjects: ['_project_1', '_abc_', '_project_'],
+ mockProjects: [
+ { id: 1, name: '_project_1', refsUrl: '/_project_1/refs' },
+ { id: 2, name: '_abc_', refsUrl: '/_abc_/refs' },
+ { id: 3, name: '_project_', refsUrl: '/_project_/refs' },
+ ],
};
diff --git a/spec/frontend/projects/commit/store/getters_spec.js b/spec/frontend/projects/commit/store/getters_spec.js
index 38c45af7aa0..f45f3114550 100644
--- a/spec/frontend/projects/commit/store/getters_spec.js
+++ b/spec/frontend/projects/commit/store/getters_spec.js
@@ -29,9 +29,15 @@ describe('Commit form modal getters', () => {
});
it('should provide a uniq list of projects', () => {
- const projects = ['_project_', '_project_', '_some_other_project'];
+ const projects = [
+ { id: 1, name: '_project_', refsUrl: '/_project_/refs' },
+ { id: 1, name: '_project_', refsUrl: '/_project_/refs' },
+ { id: 3, name: '_some_other_project', refsUrl: '/_some_other_project/refs' },
+ ];
const state = { projects };
+ expect(state.projects.length).toBe(3);
+ expect(getters.sortedProjects(state).length).toBe(2);
expect(getters.sortedProjects(state)).toEqual(projects.slice(1));
});
});
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
index 9456e6ef5f5..e49d92188ed 100644
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { loadBranches } from '~/projects/commit_box/info/load_branches';
const mockCommitPath = '/commit/abcd/branches';
@@ -22,7 +23,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
</div>`);
mock = new MockAdapter(axios);
- mock.onGet(mockCommitPath).reply(200, mockBranchesRes);
+ mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes);
});
it('loads and renders branches info', async () => {
@@ -45,7 +46,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
beforeEach(() => {
mock
.onGet(mockCommitPath)
- .reply(200, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>');
+ .reply(HTTP_STATUS_OK, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>');
});
it('displays sanitized html', async () => {
@@ -60,7 +61,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
describe('when branches request fails', () => {
beforeEach(() => {
- mock.onGet(mockCommitPath).reply(500, 'Error!');
+ mock.onGet(mockCommitPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Error!');
});
it('attempts to load and renders an error', async () => {
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index 930b801af71..bae9c48fc1e 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
@@ -51,7 +52,7 @@ describe('Project commits actions', () => {
state.projectId = '8';
const data = [{ id: 1 }];
- mock.onGet(path).replyOnce(200, data);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, data);
testAction(
actions.fetchAuthors,
null,
@@ -63,7 +64,7 @@ describe('Project commits actions', () => {
it('dispatches request/receive on error', () => {
const path = '/-/autocomplete/users.json';
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
testAction(actions.fetchAuthors, null, state, [], [{ type: 'receiveAuthorsError' }]);
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index c21c0f4f9d1..53763bd7d8f 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -4,6 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
const defaultProps = {
@@ -50,7 +51,7 @@ describe('RevisionDropdown component', () => {
const Branches = ['branch-1', 'branch-2'];
const Tags = ['tag-1', 'tag-2', 'tag-3'];
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches,
Tags,
});
@@ -64,7 +65,7 @@ describe('RevisionDropdown component', () => {
});
it('sets branches and tags to be an empty array when no tags or branches are given', async () => {
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches: undefined,
Tags: undefined,
});
@@ -76,7 +77,7 @@ describe('RevisionDropdown component', () => {
});
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
await wrapper.vm.fetchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index d598bafea92..db4a1158996 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -4,6 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
@@ -49,7 +50,7 @@ describe('RevisionDropdown component', () => {
const Branches = ['branch-1', 'branch-2'];
const Tags = ['tag-1', 'tag-2', 'tag-3'];
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches,
Tags,
});
@@ -62,7 +63,7 @@ describe('RevisionDropdown component', () => {
});
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
@@ -88,7 +89,7 @@ describe('RevisionDropdown component', () => {
describe('search', () => {
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
diff --git a/spec/frontend/projects/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index eec54dd04bc..efc9d411a98 100644
--- a/spec/frontend/projects/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProjectFindFile from '~/projects/project_find_file';
jest.mock('~/lib/dompurify', () => ({
@@ -60,7 +61,7 @@ describe('ProjectFindFile', () => {
element = $(TEMPLATE);
mock.onGet(FILE_FIND_URL).replyOnce(
- 200,
+ HTTP_STATUS_OK,
files.map((x) => x.path),
);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index d69bfc4ec92..8a1e9904a3f 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -9,6 +9,7 @@ describe('New Project', () => {
let $projectPath;
let $projectName;
let $projectNameError;
+ let $projectNameDescription;
const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
const mockChange = (el) => el.dispatchEvent(new Event('change'));
@@ -31,7 +32,8 @@ describe('New Project', () => {
</div>
</div>
<input id="project_name" />
- <div class="gl-field-error hidden" id="project_name_error" />
+ <small id="js-project-name-description" />
+ <div class="gl-field-error gl-display-none" id="js-project-name-error" />
<input id="project_path" />
</div>
<div class="js-user-readme-repo"></div>
@@ -44,7 +46,8 @@ 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');
+ $projectNameError = document.querySelector('#js-project-name-error');
+ $projectNameDescription = document.querySelector('#js-project-name-description');
});
afterEach(() => {
@@ -98,7 +101,7 @@ describe('New Project', () => {
});
it('no error message by default', () => {
- expect($projectNameError.classList.contains('hidden')).toBe(true);
+ expect($projectNameError.classList.contains('gl-display-none')).toBe(true);
});
it('show error message if name is validate', () => {
@@ -106,15 +109,16 @@ describe('New Project', () => {
triggerEvent($projectName, 'change');
expect($projectNameError.innerText).toBe(
- "Name must start with a letter, digit, emoji, or '_'",
+ 'Name must start with a letter, digit, emoji, or underscore.',
);
- expect($projectNameError.classList.contains('hidden')).toBe(false);
+ expect($projectNameError.classList.contains('gl-display-none')).toBe(false);
+ expect($projectNameDescription.classList.contains('gl-display-none')).toBe(true);
});
});
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 '_'";
+ const errormsg = 'Name must start with a letter, digit, emoji, or underscore.';
it("'.foo' should error", () => {
const text = '.foo';
expect(checkRules(text)).toBe(errormsg);
@@ -127,7 +131,7 @@ describe('New Project', () => {
describe("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces", () => {
const errormsg =
- "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces";
+ 'Name can contain only lowercase or uppercase letters, digits, emojis, spaces, dots, underscores, dashes, or pluses.';
it("'foo(#^.^#)foo' should error", () => {
const text = 'foo(#^.^#)foo';
expect(checkRules(text)).toBe(errormsg);
diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
new file mode 100644
index 00000000000..b345f264ca7
--- /dev/null
+++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
@@ -0,0 +1,72 @@
+import { GlButton, GlModal, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { s__ } from '~/locale';
+import PruneObjectsButton from '~/projects/prune_unreachable_objects_button.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
+
+describe('Project remove modal', () => {
+ let wrapper;
+
+ const findFormElement = () => wrapper.find('form');
+ const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findBtn = () => wrapper.findComponent(GlButton);
+ const defaultProps = {
+ pruneObjectsPath: 'prunepath',
+ pruneObjectsDocPath: 'prunedocspath',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(PruneObjectsButton, {
+ propsData: defaultProps,
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('intialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets a csrf token on the authenticity form input', () => {
+ expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token');
+ });
+
+ it('sets the form action to the provided path', () => {
+ expect(findFormElement().attributes('action')).toEqual(defaultProps.pruneObjectsPath);
+ });
+
+ it('sets the documentation link to the provided path', () => {
+ expect(findModal().findComponent(GlLink).attributes('href')).toEqual(
+ defaultProps.pruneObjectsDocPath,
+ );
+ });
+
+ it('button opens modal', () => {
+ const buttonModalDirective = getBinding(findBtn().element, 'gl-modal');
+
+ expect(findModal().props('modalId')).toBe(buttonModalDirective.value);
+ expect(findModal().text()).toContain(s__('UpdateProject|Are you sure you want to prune?'));
+ });
+ });
+
+ describe('when the modal is confirmed', () => {
+ beforeEach(() => {
+ createComponent();
+ findModal().vm.$emit('ok');
+ });
+
+ it('submits the form element', () => {
+ expect(findFormElement().element.submit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js b/spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js
index 35b10375821..de0c889e8c9 100644
--- a/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js
+++ b/spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js
@@ -3,14 +3,14 @@ import { GlDropdownItem } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ReportAbuseDropdownItem from '~/projects/merge_requests/components/report_abuse_dropdown_item.vue';
+import ReportAbuseDropdownItem from '~/projects/report_abuse/components/report_abuse_dropdown_item.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
describe('ReportAbuseDropdownItem', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = '1';
+ const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
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 bc373d9deb7..714e0df596e 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
@@ -1,23 +1,21 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import * as util from '~/lib/utils/url_utility';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RuleView from '~/projects/settings/branch_rules/components/view/index.vue';
+import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
import {
I18N,
ALL_BRANCHES_WILDCARD,
} from '~/projects/settings/branch_rules/components/view/constants';
-import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
-import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
import {
branchProtectionsMockResponse,
- approvalRulesMock,
- statusChecksRulesMock,
matchingBranchesCount,
-} from './mock_data';
+} from 'ee_else_ce_jest/projects/settings/branch_rules/components/view/mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
@@ -29,18 +27,18 @@ Vue.use(VueApollo);
const protectionMockProps = {
headerLinkHref: 'protected/branches',
- headerLinkTitle: 'Manage in Protected Branches',
- roles: [{ accessLevelDescription: 'Maintainers' }],
- users: [{ avatarUrl: 'test.com/user.png', name: 'peter', webUrl: 'test.com' }],
+ headerLinkTitle: I18N.manageProtectionsLinkTitle,
};
+const roles = [
+ { accessLevelDescription: 'Maintainers' },
+ { accessLevelDescription: 'Maintainers + Developers' },
+];
describe('View branch rules', () => {
let wrapper;
let fakeApollo;
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
- const approvalRulesPath = 'approval/rules';
- const statusChecksPath = 'status/checks';
const branchProtectionsMockRequestHandler = jest
.fn()
.mockResolvedValue(branchProtectionsMockResponse);
@@ -50,7 +48,8 @@ describe('View branch rules', () => {
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
- provide: { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath },
+ provide: { projectPath, protectedBranchesPath },
+ stubs: { Protection },
});
await waitForPromises();
@@ -106,41 +105,53 @@ describe('View branch rules', () => {
it('renders a branch protection component for push rules', () => {
expect(findBranchProtections().at(0).props()).toMatchObject({
- header: sprintf(I18N.allowedToPushHeader, { total: 2 }),
+ header: sprintf(I18N.allowedToPushHeader, {
+ total: 2,
+ }),
...protectionMockProps,
});
});
+ it('passes expected roles for push rules via props', () => {
+ findBranchProtections()
+ .at(0)
+ .props()
+ .roles.forEach((role, i) => {
+ expect(role).toMatchObject({
+ accessLevelDescription: roles[i].accessLevelDescription,
+ });
+ });
+ });
+
it('renders force push protection', () => {
expect(findForcePushTitle().exists()).toBe(true);
});
it('renders a branch protection component for merge rules', () => {
expect(findBranchProtections().at(1).props()).toMatchObject({
- header: sprintf(I18N.allowedToMergeHeader, { total: 2 }),
+ header: sprintf(I18N.allowedToMergeHeader, {
+ total: 2,
+ }),
...protectionMockProps,
});
});
- it('renders a branch protection component for approvals', () => {
- expect(findApprovalsTitle().exists()).toBe(true);
-
- expect(findBranchProtections().at(2).props()).toMatchObject({
- header: sprintf(I18N.approvalsHeader, { total: 3 }),
- headerLinkHref: approvalRulesPath,
- headerLinkTitle: I18N.manageApprovalsLinkTitle,
- approvals: approvalRulesMock,
- });
+ it('passes expected roles form merge rules via props', () => {
+ findBranchProtections()
+ .at(1)
+ .props()
+ .roles.forEach((role, i) => {
+ expect(role).toMatchObject({
+ accessLevelDescription: roles[i].accessLevelDescription,
+ });
+ });
});
- it('renders a branch protection component for status checks', () => {
- expect(findStatusChecksTitle().exists()).toBe(true);
+ it('does not render a branch protection component for approvals', () => {
+ expect(findApprovalsTitle().exists()).toBe(false);
+ });
- expect(findBranchProtections().at(3).props()).toMatchObject({
- header: sprintf(I18N.statusChecksHeader, { total: 2 }),
- headerLinkHref: statusChecksPath,
- headerLinkTitle: I18N.statusChecksLinkTitle,
- statusChecks: statusChecksRulesMock,
- });
+ it('does not render a branch protection component for status checks', () => {
+ expect(findStatusChecksTitle().exists()).toBe(false);
});
});
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 821dba75b62..c64af7767cc 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
@@ -85,16 +85,8 @@ export 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',
- },
+ accessLevel: 30,
+ accessLevelDescription: 'Maintainers',
},
},
{
@@ -102,9 +94,7 @@ export const accessLevelsMockResponse = [
node: {
__typename: 'PushAccessLevel',
accessLevel: 40,
- accessLevelDescription: 'Maintainers',
- group: null,
- user: null,
+ accessLevelDescription: 'Maintainers + Developers',
},
},
];
@@ -122,10 +112,10 @@ export const branchProtectionsMockResponse = {
{
__typename: 'BranchRule',
name: 'main',
+ matchingBranchesCount,
branchProtection: {
__typename: 'BranchProtection',
allowForcePush: true,
- codeOwnerApprovalRequired: true,
mergeAccessLevels: {
__typename: 'MergeAccessLevelConnection',
edges: accessLevelsMockResponse,
@@ -135,41 +125,23 @@ export const branchProtectionsMockResponse = {
edges: accessLevelsMockResponse,
},
},
- approvalRules: {
- __typename: 'ApprovalProjectRuleConnection',
- nodes: approvalRulesMock,
- },
- externalStatusChecks: {
- __typename: 'ExternalStatusCheckConnection',
- nodes: statusChecksRulesMock,
- },
- matchingBranchesCount,
},
{
__typename: 'BranchRule',
name: '*',
+ matchingBranchesCount,
branchProtection: {
__typename: 'BranchProtection',
allowForcePush: true,
- codeOwnerApprovalRequired: true,
mergeAccessLevels: {
__typename: 'MergeAccessLevelConnection',
- edges: [],
+ edges: accessLevelsMockResponse,
},
pushAccessLevels: {
__typename: 'PushAccessLevelConnection',
- edges: [],
+ edges: accessLevelsMockResponse,
},
},
- approvalRules: {
- __typename: 'ApprovalProjectRuleConnection',
- nodes: [],
- },
- externalStatusChecks: {
- __typename: 'ExternalStatusCheckConnection',
- nodes: [],
- },
- matchingBranchesCount,
},
],
},
diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
index bfbf3e234f4..ca9a72663d2 100644
--- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -34,6 +34,7 @@ describe('projects/settings/components/default_branch_selector', () => {
projectId,
refType: null,
state: true,
+ toggleButtonClass: null,
translations: {
dropdownHeader: expect.any(String),
searchPlaceholder: expect.any(String),
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index 1b06f7874a3..26297d0c3ff 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -5,6 +5,7 @@ import {
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { last } from 'lodash';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -254,7 +255,6 @@ describe('Access Level Dropdown', () => {
createComponent({ preselectedItems });
await waitForPromises();
- const spy = jest.spyOn(wrapper.vm, '$emit');
const dropdownItems = findAllDropdownItems();
// select new item from each group
findDropdownItemWithText(dropdownItems, 'role1').trigger('click');
@@ -267,7 +267,7 @@ describe('Access Level Dropdown', () => {
findDropdownItemWithText(dropdownItems, 'user8').trigger('click');
findDropdownItemWithText(dropdownItems, 'key11').trigger('click');
- expect(spy).toHaveBeenLastCalledWith('select', [
+ expect(last(wrapper.emitted('select'))[0]).toStrictEqual([
{ access_level: 1 },
{ id: 112, access_level: 2, _destroy: true },
{ id: 113, access_level: 3 },
@@ -347,12 +347,10 @@ describe('Access Level Dropdown', () => {
});
it('should emit `hidden` event with dropdown selection', () => {
- jest.spyOn(wrapper.vm, '$emit');
-
findAllDropdownItems().at(1).trigger('click');
findDropdown().vm.$emit('hidden');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('hidden', [{ access_level: 2 }]);
+ expect(wrapper.emitted('hidden')[0][0]).toStrictEqual([{ access_level: 2 }]);
});
});
});
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 329060b9d10..f82ad80135e 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -4,6 +4,7 @@ import MockAxiosAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
const TEST_UPDATE_PATH = '/test/update_shared_runners';
@@ -36,7 +37,7 @@ describe('projects/settings/components/shared_runners', () => {
beforeEach(() => {
mockAxios = new MockAxiosAdapter(axios);
- mockAxios.onPost(TEST_UPDATE_PATH).reply(200);
+ mockAxios.onPost(TEST_UPDATE_PATH).reply(HTTP_STATUS_OK);
});
afterEach(() => {
@@ -132,7 +133,9 @@ describe('projects/settings/components/shared_runners', () => {
describe('when request encounters an error', () => {
it('should show custom error message from API if it exists', async () => {
- mockAxios.onPost(TEST_UPDATE_PATH).reply(401, { error: 'Custom API Error message' });
+ mockAxios
+ .onPost(TEST_UPDATE_PATH)
+ .reply(HTTP_STATUS_UNAUTHORIZED, { error: 'Custom API Error message' });
createComponent();
expect(getToggleValue()).toBe(false);
@@ -144,7 +147,7 @@ describe('projects/settings/components/shared_runners', () => {
});
it('should show default error message if API does not return a custom error message', async () => {
- mockAxios.onPost(TEST_UPDATE_PATH).reply(401);
+ mockAxios.onPost(TEST_UPDATE_PATH).reply(HTTP_STATUS_UNAUTHORIZED);
createComponent();
expect(getToggleValue()).toBe(false);
diff --git a/spec/frontend/projects/settings/mock_data.js b/spec/frontend/projects/settings/mock_data.js
index 0262c0e3e43..86e5396bd25 100644
--- a/spec/frontend/projects/settings/mock_data.js
+++ b/spec/frontend/projects/settings/mock_data.js
@@ -47,11 +47,7 @@ export const pushAccessLevelsMockResult = {
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 447d7e86ceb..56b39f04580 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -1,9 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
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 BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
@@ -11,8 +12,19 @@ import {
branchRulesMockResponse,
appProvideMock,
} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
+import {
+ I18N,
+ BRANCH_PROTECTION_MODAL_ID,
+ PROTECTED_BRANCHES_ANCHOR,
+} from '~/projects/settings/repository/branch_rules/constants';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { expandSection } from '~/settings_panels';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
+jest.mock('~/settings_panels');
+jest.mock('~/lib/utils/common_utils');
Vue.use(VueApollo);
@@ -28,6 +40,8 @@ describe('Branch rules app', () => {
wrapper = mountExtended(BranchRules, {
apolloProvider: fakeApollo,
provide: appProvideMock,
+ stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
+ directives: { GlModal: createMockDirective() },
});
await waitForPromises();
@@ -35,17 +49,19 @@ describe('Branch rules app', () => {
const findAllBranchRules = () => wrapper.findAllComponents(BranchRule);
const findEmptyState = () => wrapper.findByTestId('empty');
+ const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule);
+ const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => createComponent());
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError });
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(findEmptyState().text()).toBe(i18n.emptyState);
+ expect(findEmptyState().text()).toBe(I18N.emptyState);
});
it('renders branch rules', () => {
@@ -61,4 +77,38 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
+
+ describe('Add branch rule', () => {
+ it('renders an Add branch rule button', () => {
+ expect(findAddBranchRuleButton().exists()).toBe(true);
+ });
+
+ it('renders a modal with correct props/attributes', () => {
+ expect(findModal().props()).toMatchObject({
+ modalId: BRANCH_PROTECTION_MODAL_ID,
+ title: I18N.addBranchRule,
+ });
+
+ expect(findModal().attributes('ok-title')).toBe(I18N.createProtectedBranch);
+ });
+
+ it('renders correct modal id for the default action', () => {
+ const binding = getBinding(findAddBranchRuleButton().element, 'gl-modal');
+
+ expect(binding.value).toBe(BRANCH_PROTECTION_MODAL_ID);
+ });
+
+ it('renders the correct modal content', () => {
+ expect(findModal().text()).toContain(I18N.branchRuleModalDescription);
+ expect(findModal().text()).toContain(I18N.branchRuleModalContent);
+ });
+
+ it('when the primary modal action is clicked, takes user to the correct location', () => {
+ findAddBranchRuleButton().trigger('click');
+ findModal().vm.$emit('ok');
+
+ expect(expandSection).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ expect(scrollToElement).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ });
+ });
});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index a079b0b97fd..3852f2678b7 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PANEL_STATE from '~/prometheus_metrics/constants';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
@@ -13,7 +14,7 @@ describe('PrometheusMetrics', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(customMetricsEndpoint).reply(200, {
+ mock.onGet(customMetricsEndpoint).reply(HTTP_STATUS_OK, {
metrics,
});
loadHTMLFixture(FIXTURE);
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index a65cbe1a47a..45654d6a2eb 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PANEL_STATE from '~/prometheus_metrics/constants';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
@@ -116,7 +117,7 @@ describe('PrometheusMetrics', () => {
let mock;
function mockSuccess() {
- mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(HTTP_STATUS_OK, {
data: metrics,
success: true,
});
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 0aec4fbc037..b4029d94980 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
jest.mock('~/flash');
@@ -115,7 +116,9 @@ describe('ProtectedBranchEdit', () => {
describe('when clicked', () => {
beforeEach(async () => {
- mock.onPatch(TEST_URL, { protected_branch: { [patchParam]: true } }).replyOnce(200, {});
+ mock
+ .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
+ .replyOnce(HTTP_STATUS_OK, {});
});
it('checks and disables button', async () => {
@@ -142,7 +145,7 @@ describe('ProtectedBranchEdit', () => {
describe('when clicked and BE error', () => {
beforeEach(() => {
- mock.onPatch(TEST_URL).replyOnce(500);
+ mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
toggle.click();
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 4997c13bbb2..40d3a291074 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -1,5 +1,4 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -8,8 +7,13 @@ import Vuex from 'vuex';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
import tags from 'test_fixtures/api/tags/tags.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
-import { ENTER_KEY } from '~/lib/utils/keys';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import {
@@ -37,7 +41,7 @@ describe('Ref selector component', () => {
let requestSpies;
const createComponent = (mountOverrides = {}, propsData = {}) => {
- wrapper = mount(
+ wrapper = mountExtended(
RefSelector,
merge(
{
@@ -52,9 +56,6 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: selectedRef });
},
},
- stubs: {
- GlSearchBoxByType: true,
- },
store: createStore(),
},
mountOverrides,
@@ -68,9 +69,11 @@ describe('Ref selector component', () => {
branchesApiCallSpy = jest
.fn()
- .mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
- tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
- commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
+ tagsApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
mock
@@ -84,76 +87,63 @@ describe('Ref selector component', () => {
.reply((config) => commitApiCallSpy(config));
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
//
// Finders
//
- const findButtonContent = () => wrapper.find('button');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle');
- const findNoResults = () => wrapper.find('[data-testid="no-results"]');
+ const findNoResults = () => wrapper.findByTestId('listbox-no-results-text');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findListBoxSection = (section) => {
+ const foundSections = wrapper
+ .findAll('[role="group"]')
+ .filter((ul) => ul.text().includes(section));
+ return foundSections.length > 0 ? foundSections.at(0) : foundSections;
+ };
+
+ const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
- const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
- const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
- const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
+ const findBranchesSection = () => findListBoxSection('Branches');
+ const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
- const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
- const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
- const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
+ const findTagsSection = () => findListBoxSection('Tags');
- const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
- const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
- const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
+ const findCommitsSection = () => findListBoxSection('Commits');
- const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
+ const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
//
// Expecters
//
- const branchesSectionContainsErrorMessage = () => {
- const branchesSection = findBranchesSection();
+ const sectionContainsErrorMessage = (message) => {
+ const errorSection = findErrorListWrapper();
- return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
- };
-
- const tagsSectionContainsErrorMessage = () => {
- const tagsSection = findTagsSection();
-
- return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
- };
-
- const commitsSectionContainsErrorMessage = () => {
- const commitsSection = findCommitsSection();
-
- return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
+ return errorSection ? errorSection.text().includes(message) : false;
};
//
// Convenience methods
//
const updateQuery = (newQuery) => {
- findSearchBox().vm.$emit('input', newQuery);
+ findListbox().vm.$emit('search', newQuery);
};
const selectFirstBranch = async () => {
- findFirstBranchDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.branches[0].name);
await nextTick();
};
const selectFirstTag = async () => {
- findFirstTagDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.tags[0].name);
await nextTick();
};
const selectFirstCommit = async () => {
- findFirstCommitDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.commit.id);
await nextTick();
};
@@ -188,7 +178,7 @@ describe('Ref selector component', () => {
});
describe('when name property is provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
const name = 'default_tag';
createComponent({ propsData: { name } });
@@ -198,7 +188,7 @@ describe('Ref selector component', () => {
});
describe('when name property is not provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
createComponent();
expect(findHiddenInputField().exists()).toBe(false);
@@ -217,7 +207,7 @@ describe('Ref selector component', () => {
});
it('adds the provided ID to the GlDropdown instance', () => {
- expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id);
+ expect(findListbox().attributes().id).toBe(id);
});
});
@@ -231,7 +221,7 @@ describe('Ref selector component', () => {
});
it('renders the pre-selected ref name', () => {
- expect(findButtonContent().text()).toBe(preselectedRef);
+ expect(findButtonToggle().text()).toBe(preselectedRef);
});
it('binds hidden input field to the pre-selected ref', () => {
@@ -252,7 +242,7 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: updatedRef });
await nextTick();
- expect(findButtonContent().text()).toBe(updatedRef);
+ expect(findButtonToggle().text()).toBe(updatedRef);
});
});
@@ -289,28 +279,13 @@ describe('Ref selector component', () => {
});
});
- describe('when the Enter is pressed', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests({ andClearMocks: true });
- });
-
- it('requeries the endpoints when Enter is pressed', () => {
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- return waitForRequests().then(() => {
- expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
- expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
- });
- });
- });
-
describe('when no results are found', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
- tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
- commitApiCallSpy = jest.fn().mockReturnValue([404]);
+ branchesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
+ tagsApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_NOT_FOUND]);
createComponent();
@@ -348,27 +323,10 @@ describe('Ref selector component', () => {
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
- expect(findBranchesSection().props('shouldShowCheck')).toBe(true);
- });
-
- it('renders the "Branches" heading with a total number indicator', () => {
- expect(
- findBranchesSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Branches 123');
});
it("does not render an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each non-default branch as a selectable item', () => {
- const dropdownItems = findBranchDropdownItems();
-
- fixtures.branches.forEach((b, i) => {
- if (!b.default) {
- expect(dropdownItems.at(i).text()).toBe(b.name);
- }
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders the default branch as a selectable item with a "default" badge', () => {
@@ -385,7 +343,9 @@ describe('Ref selector component', () => {
describe('when the branches search returns no results', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ branchesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -399,7 +359,7 @@ describe('Ref selector component', () => {
describe('when the branches search returns an error', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([500]);
+ branchesApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -407,11 +367,11 @@ describe('Ref selector component', () => {
});
it('renders the branches section in the dropdown', () => {
- expect(findBranchesSection().exists()).toBe(true);
+ expect(findBranchesSection().exists()).toBe(false);
});
it("renders an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.branchesErrorMessage)).toBe(true);
});
});
});
@@ -426,31 +386,24 @@ describe('Ref selector component', () => {
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
- expect(findTagsSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
- expect(
- findTagsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Tags 456');
+ expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Tags ${fixtures.tags.length}`,
+ );
});
it("does not render an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each tag as a selectable item', () => {
- const dropdownItems = findTagDropdownItems();
-
- fixtures.tags.forEach((t, i) => {
- expect(dropdownItems.at(i).text()).toBe(t.name);
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
describe('when the tags search returns no results', () => {
beforeEach(() => {
- tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ tagsApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -464,7 +417,7 @@ describe('Ref selector component', () => {
describe('when the tags search returns an error', () => {
beforeEach(() => {
- tagsApiCallSpy = jest.fn().mockReturnValue([500]);
+ tagsApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -472,11 +425,11 @@ describe('Ref selector component', () => {
});
it('renders the tags section in the dropdown', () => {
- expect(findTagsSection().exists()).toBe(true);
+ expect(findTagsSection().exists()).toBe(false);
});
it("renders an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.tagsErrorMessage)).toBe(true);
});
});
});
@@ -496,25 +449,19 @@ describe('Ref selector component', () => {
});
it('renders the "Commits" heading with a total number indicator', () => {
- expect(
- findCommitsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Commits 1');
- });
-
- it("does not render an error message in the comits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(false);
+ expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Commits 1`,
+ );
});
- it('renders each commit as a selectable item with the short SHA and commit title', () => {
- const dropdownItems = findCommitDropdownItems();
-
- expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
+ it("does not render an error message in the commits section's body", () => {
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
describe('when the commit search returns no results (i.e. a 404)', () => {
beforeEach(() => {
- commitApiCallSpy = jest.fn().mockReturnValue([404]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_NOT_FOUND]);
createComponent();
@@ -530,7 +477,7 @@ describe('Ref selector component', () => {
describe('when the commit search returns an error (other than a 404)', () => {
beforeEach(() => {
- commitApiCallSpy = jest.fn().mockReturnValue([500]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -540,11 +487,11 @@ describe('Ref selector component', () => {
});
it('renders the commits section in the dropdown', () => {
- expect(findCommitsSection().exists()).toBe(true);
+ expect(findCommitsSection().exists()).toBe(false);
});
it("renders an error message in the commits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true);
});
});
});
@@ -558,26 +505,13 @@ describe('Ref selector component', () => {
return waitForRequests();
});
- it('renders a checkmark by the selected item', async () => {
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass(
- 'gl-visibility-hidden',
- );
-
- await selectFirstBranch();
-
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass(
- 'gl-visibility-hidden',
- );
- });
-
- describe('when a branch is seleceted', () => {
+ describe('when a branch is selected', () => {
it("displays the branch name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstBranch();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
});
it("updates the v-model binding with the branch's name", async () => {
@@ -591,12 +525,11 @@ describe('Ref selector component', () => {
describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstTag();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
});
it("updates the v-model binding with the tag's name", async () => {
@@ -610,12 +543,11 @@ describe('Ref selector component', () => {
describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstCommit();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.commit.id);
+ expect(findButtonToggle().text()).toBe(fixtures.commit.id);
});
it("updates the v-model binding with the commit's full SHA", async () => {
@@ -675,21 +607,6 @@ describe('Ref selector component', () => {
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
- it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
- createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } });
- updateQuery('abcd1234');
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(true);
- expect(findCommitsSection().exists()).toBe(true);
-
- wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(false);
- expect(findCommitsSection().exists()).toBe(true);
- });
-
it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
@@ -713,8 +630,7 @@ describe('Ref selector component', () => {
describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!';
- const isInvalidClassApplied = () =>
- wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass];
+ const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
describe('valid state', () => {
describe('when the state prop is not provided', () => {
diff --git a/spec/frontend/ref/format_refs_spec.js b/spec/frontend/ref/format_refs_spec.js
new file mode 100644
index 00000000000..6dd49574721
--- /dev/null
+++ b/spec/frontend/ref/format_refs_spec.js
@@ -0,0 +1,38 @@
+import { formatListBoxItems, formatErrors } from '~/ref/format_refs';
+import { DEFAULT_I18N } from '~/ref/constants';
+import {
+ MOCK_BRANCHES,
+ MOCK_COMMITS,
+ MOCK_ERROR,
+ MOCK_TAGS,
+ FORMATTED_BRANCHES,
+ FORMATTED_TAGS,
+ FORMATTED_COMMITS,
+} from './mock_data';
+
+describe('formatListBoxItems', () => {
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_BRANCHES} | ${MOCK_TAGS} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_TAGS, FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_COMMITS]}
+ ${[]} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${undefined} | ${undefined} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${undefined} | ${null} | ${[FORMATTED_BRANCHES]}
+ `('should correctly format listbox items', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatListBoxItems(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
+
+describe('formatErrors', () => {
+ const { branchesErrorMessage, tagsErrorMessage, commitsErrorMessage } = DEFAULT_I18N;
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_ERROR} | ${MOCK_ERROR} | ${MOCK_ERROR} | ${[branchesErrorMessage, tagsErrorMessage, commitsErrorMessage]}
+ ${MOCK_ERROR} | ${[]} | ${MOCK_ERROR} | ${[branchesErrorMessage, commitsErrorMessage]}
+ ${[]} | ${[]} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${undefined} | ${undefined} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${MOCK_ERROR} | ${undefined} | ${null} | ${[branchesErrorMessage]}
+ `('should correctly format listbox errors', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatErrors(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
diff --git a/spec/frontend/ref/mock_data.js b/spec/frontend/ref/mock_data.js
new file mode 100644
index 00000000000..c02d4da7aed
--- /dev/null
+++ b/spec/frontend/ref/mock_data.js
@@ -0,0 +1,87 @@
+export const MOCK_BRANCHES = [
+ {
+ default: true,
+ name: 'main',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test1',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test2',
+ value: undefined,
+ },
+];
+
+export const MOCK_TAGS = [
+ {
+ name: 'test_tag',
+ value: undefined,
+ },
+ {
+ name: 'test_tag2',
+ value: undefined,
+ },
+];
+
+export const MOCK_COMMITS = [
+ {
+ name: 'test_commit',
+ value: undefined,
+ },
+];
+
+export const FORMATTED_BRANCHES = {
+ text: 'Branches',
+ options: [
+ {
+ default: true,
+ text: 'main',
+ value: 'main',
+ },
+ {
+ default: false,
+ text: 'test1',
+ value: 'test1',
+ },
+ {
+ default: false,
+ text: 'test2',
+ value: 'test2',
+ },
+ ],
+};
+
+export const FORMATTED_TAGS = {
+ text: 'Tags',
+ options: [
+ {
+ text: 'test_tag',
+ value: 'test_tag',
+ default: undefined,
+ },
+ {
+ text: 'test_tag2',
+ value: 'test_tag2',
+ default: undefined,
+ },
+ ],
+};
+
+export const FORMATTED_COMMITS = {
+ text: 'Commits',
+ options: [
+ {
+ text: 'test_commit',
+ value: 'test_commit',
+ default: undefined,
+ },
+ ],
+};
+
+export const MOCK_ERROR = {
+ error: new Error('test_error'),
+};
diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js
index f6a13856042..f7333bf6893 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -1,8 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
-import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
+import { PathIdSeparator } from '~/related_issues/constants';
jest.mock('ee_else_ce/gfm_auto_complete', () => {
return function gfmAutoComplete() {
@@ -21,7 +22,7 @@ describe('RelatedIssuableInput', () => {
inputValue: '',
references: [],
pathIdSeparator: PathIdSeparator.Issue,
- issuableType: issuableTypesMap.issue,
+ issuableType: TYPE_ISSUE,
autoCompleteSources: {
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
},
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 649d8eef6ec..bd61e4537f9 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -5,11 +5,13 @@ import Vuex from 'vuex';
import { nextTick } from 'vue';
import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
+import { putCreateReleaseNotification } from '~/releases/release_notification_service';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@@ -19,6 +21,8 @@ const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.r
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
const upcomingReleaseDocsPath = 'path/to/upcoming/release/docs';
+const projectPath = 'project/path';
+jest.mock('~/releases/release_notification_service');
describe('Release edit/new component', () => {
let wrapper;
@@ -32,6 +36,7 @@ describe('Release edit/new component', () => {
state = {
release,
isExistingRelease: true,
+ projectPath,
markdownDocsPath: 'path/to/markdown/docs',
releasesPagePath,
projectId: '8',
@@ -91,7 +96,7 @@ describe('Release edit/new component', () => {
mock = new MockAdapter(axios);
gon.api_version = 'v4';
- mock.onGet('/api/v4/projects/8/milestones').reply(200, originalMilestones);
+ mock.onGet('/api/v4/projects/8/milestones').reply(HTTP_STATUS_OK, originalMilestones);
release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data;
});
@@ -125,7 +130,7 @@ describe('Release edit/new component', () => {
it('renders the description text at the top of the page', () => {
expect(wrapper.find('.js-subtitle-text').text()).toBe(
- 'Releases are based on Git tags. We recommend tags that use semantic versioning, for example v1.0.0, v2.1.0-pre.',
+ 'Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.',
);
});
@@ -163,6 +168,13 @@ describe('Release edit/new component', () => {
expect(actions.saveRelease).toHaveBeenCalledTimes(1);
});
+
+ it('sets release created notification when the form is submitted', () => {
+ findForm().trigger('submit');
+ const releaseName = originalOneReleaseForEditingQueryResponse.data.project.release.name;
+ expect(putCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(putCreateReleaseNotification).toHaveBeenCalledWith(projectPath, releaseName);
+ });
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 48589a54ec4..ef3bd5ca873 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -92,10 +92,6 @@ describe('app_index.vue', () => {
queryMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
// Finders
const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
@@ -179,12 +175,14 @@ describe('app_index.vue', () => {
expect(findPagination().exists()).toBe(pagination);
});
- it('does render the "New release" button', () => {
- expect(findNewReleaseButton().exists()).toBe(true);
+ it('does render the "New release" button only for non-empty state', () => {
+ const shouldRenderNewReleaseButton = !emptyState;
+ expect(findNewReleaseButton().exists()).toBe(shouldRenderNewReleaseButton);
});
- it('does render the sort controls', () => {
- expect(findSort().exists()).toBe(true);
+ it('does render the sort controls only for non-empty state', () => {
+ const shouldRenderControls = !emptyState;
+ expect(findSort().exists()).toBe(shouldRenderControls);
});
},
);
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index c5cb8589ee8..efe72e8000a 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -5,12 +5,14 @@ import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/quer
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
+import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql';
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
Vue.use(VueApollo);
@@ -88,6 +90,11 @@ describe('Release show component', () => {
createComponent({ apolloProvider });
});
+ it('shows info notification on mount', () => {
+ expect(popCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(popCreateReleaseNotification).toHaveBeenCalledWith(MOCK_FULL_PATH);
+ });
+
it('builds a GraphQL with the expected variables', () => {
expect(queryHandler).toHaveBeenCalledTimes(1);
expect(queryHandler).toHaveBeenCalledWith({
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 6f935215dd7..69443cb7a11 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -40,13 +40,11 @@ describe('Evidence Block', () => {
});
it('renders the correct hover text for the download', () => {
- expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Download evidence JSON');
+ expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Open evidence JSON in new tab');
});
- it('renders the correct file link for download', () => {
- expect(wrapper.findComponent(GlLink).attributes().download).toMatch(
- /v1\.1-evidences-[0-9]+\.json/,
- );
+ it('renders a link that opens in a new tab', () => {
+ expect(wrapper.findComponent(GlLink).attributes().target).toBe('_blank');
});
describe('sha text', () => {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 4f94e4dfd55..6d53bf5a49e 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -123,42 +123,14 @@ describe('Release block assets', () => {
});
});
- describe('external vs internal links', () => {
+ describe('links', () => {
const containsExternalSourceIndicator = () =>
wrapper.find('[data-testid="external-link-indicator"]').exists();
- describe('when a link is external', () => {
- beforeEach(() => {
- defaultProps.assets.sources = [];
- defaultProps.assets.links = [
- {
- ...defaultProps.assets.links[0],
- external: true,
- },
- ];
- createComponent(defaultProps);
- });
-
- it('renders the link with an "external source" indicator', () => {
- expect(containsExternalSourceIndicator()).toBe(true);
- });
- });
+ beforeEach(() => createComponent(defaultProps));
- describe('when a link is internal', () => {
- beforeEach(() => {
- defaultProps.assets.sources = [];
- defaultProps.assets.links = [
- {
- ...defaultProps.assets.links[0],
- external: false,
- },
- ];
- createComponent(defaultProps);
- });
-
- it('renders the link without the "external source" indicator', () => {
- expect(containsExternalSourceIndicator()).toBe(false);
- });
+ it('renders with an external source indicator (except for sections with no title)', () => {
+ expect(containsExternalSourceIndicator()).toBe(true);
});
});
});
diff --git a/spec/frontend/releases/components/releases_empty_state_spec.js b/spec/frontend/releases/components/releases_empty_state_spec.js
index 495e6d863f7..f0db7d16bd7 100644
--- a/spec/frontend/releases/components/releases_empty_state_spec.js
+++ b/spec/frontend/releases/components/releases_empty_state_spec.js
@@ -4,6 +4,7 @@ import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
describe('releases_empty_state.vue', () => {
const documentationPath = 'path/to/releases/documentation';
+ const newReleasePath = 'path/to/releases/new-release';
const illustrationPath = 'path/to/releases/empty/state/illustration';
let wrapper;
@@ -12,6 +13,7 @@ describe('releases_empty_state.vue', () => {
wrapper = shallowMountExtended(ReleasesEmptyState, {
provide: {
documentationPath,
+ newReleasePath,
illustrationPath,
},
});
@@ -21,36 +23,17 @@ describe('releases_empty_state.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a GlEmptyState and provides it with the correct props', () => {
const emptyStateProps = wrapper.findComponent(GlEmptyState).props();
- expect(emptyStateProps).toEqual(
- expect.objectContaining({
- title: ReleasesEmptyState.i18n.emptyStateTitle,
- svgPath: illustrationPath,
- }),
- );
- });
-
- it('renders the empty state text', () => {
- expect(wrapper.findByText(ReleasesEmptyState.i18n.emptyStateText).exists()).toBe(true);
- });
-
- it('renders a link to the documentation', () => {
- const documentationLink = wrapper.findByText(ReleasesEmptyState.i18n.moreInformation);
-
- expect(documentationLink.exists()).toBe(true);
-
- expect(documentationLink.attributes()).toEqual(
- expect.objectContaining({
- 'aria-label': ReleasesEmptyState.i18n.releasesDocumentation,
- href: documentationPath,
- target: '_blank',
- }),
- );
+ expect(emptyStateProps).toMatchObject({
+ title: ReleasesEmptyState.i18n.emptyStateTitle,
+ svgPath: illustrationPath,
+ description: ReleasesEmptyState.i18n.emptyStateText,
+ primaryButtonLink: newReleasePath,
+ primaryButtonText: ReleasesEmptyState.i18n.newRelease,
+ secondaryButtonLink: documentationPath,
+ secondaryButtonText: ReleasesEmptyState.i18n.releasesDocumentation,
+ });
});
});
diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js
new file mode 100644
index 00000000000..2344d4b929a
--- /dev/null
+++ b/spec/frontend/releases/release_notification_service_spec.js
@@ -0,0 +1,57 @@
+import {
+ popCreateReleaseNotification,
+ putCreateReleaseNotification,
+} from '~/releases/release_notification_service';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('~/releases/release_notification_service', () => {
+ const projectPath = 'test-project-path';
+ const releaseName = 'test-release-name';
+
+ const storageKey = `createRelease:${projectPath}`;
+
+ describe('prepareCreateReleaseFlash', () => {
+ it('should set the session storage with project path key and release name value', () => {
+ putCreateReleaseNotification(projectPath, releaseName);
+
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(releaseName);
+ });
+ });
+
+ describe('showNotificationsIfPresent', () => {
+ describe('if notification is prepared', () => {
+ beforeEach(() => {
+ window.sessionStorage.setItem(storageKey, releaseName);
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should remove storage key', () => {
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(null);
+ });
+
+ it('should create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${releaseName} has been successfully created.`,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+ });
+
+ describe('if notification is not prepared', () => {
+ beforeEach(() => {
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should not create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index eeee6747349..ca3b2d5f734 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -23,6 +23,8 @@ jest.mock('~/api/tags_api');
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
@@ -41,9 +43,12 @@ describe('Release edit/new actions', () => {
let releaseResponse;
let error;
+ const projectPath = 'test/project-path';
+
const setupState = (updates = {}) => {
state = {
...createState({
+ projectPath,
projectId: '18',
isExistingRelease: true,
tagName: releaseResponse.tag_name,
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 944769d22cc..bf40af9a897 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -89,6 +89,15 @@ describe('Release edit/new mutations', () => {
expect(state.release.tagName).toBe(newTag);
});
+
+ it('nulls out existing release', () => {
+ state.release = release;
+ state.existingRelease = release;
+ const newTag = 'updated-tag-name';
+ mutations[types.UPDATE_RELEASE_TAG_NAME](state, newTag);
+
+ expect(state.existingRelease).toBe(null);
+ });
});
describe(`${types.UPDATE_RELEASE_TAG_MESSAGE}`, () => {
@@ -304,6 +313,17 @@ describe('Release edit/new mutations', () => {
expect(state.tagNotes).toBe('');
expect(state.isFetchingTagNotes).toBe(false);
});
+
+ it('nulls out existing release', () => {
+ state.existingRelease = release;
+ const message = 'there was an error';
+ state.isFetchingTagNotes = true;
+ state.tagNotes = 'tag notes';
+
+ mutations[types.RECEIVE_TAG_NOTES_ERROR](state, { message });
+
+ expect(state.existingRelease).toBe(null);
+ });
});
describe(`${types.UPDATE_INCLUDE_TAG_NOTES}`, () => {
it('sets whether or not to include the tag notes', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2e8860f67ef..03a8ee6ac5d 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -16,7 +16,7 @@ import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
-import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index c23d5ae5823..f327a8cfae7 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlIcon, GlLink } from '@gitlab/ui';
+import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -16,13 +16,14 @@ describe('ForkInfo component', () => {
let wrapper;
let mockResolver;
const forkInfoError = new Error('Something went wrong');
+ const projectId = 'gid://gitlab/Project/1';
Vue.use(VueApollo);
const createCommitData = ({ ahead = 3, behind = 7 }) => {
return {
data: {
- project: { id: '1', forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
+ project: { id: projectId, forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
},
};
};
@@ -35,6 +36,7 @@ describe('ForkInfo component', () => {
wrapper = shallowMountExtended(ForkInfo, {
apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]),
propsData: { ...propsForkInfo, ...props },
+ stubs: { GlSprintf },
});
return waitForPromises();
};
@@ -42,8 +44,10 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
- const findDivergenceMessage = () => wrapper.find('.gl-text-secondary');
+ const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
+ const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink);
+
it('displays a skeleton while loading data', async () => {
createComponent();
expect(findSkeleton().exists()).toBe(true);
@@ -88,28 +92,54 @@ describe('ForkInfo component', () => {
expect(findDivergenceMessage().text()).toBe(i18n.unknown);
});
- it('shows correct divergence message when data is present', async () => {
- await createComponent();
- expect(findDivergenceMessage().text()).toMatchInterpolatedText(
- '7 commits behind, 3 commits ahead of the upstream repository.',
- );
- });
-
it('renders up to date message when divergence is unknown', async () => {
await createComponent({}, { ahead: 0, behind: 0 });
expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
});
- it('renders commits ahead message', async () => {
- await createComponent({}, { behind: 0 });
- expect(findDivergenceMessage().text()).toBe('3 commits ahead of the upstream repository.');
- });
-
- it('renders commits behind message', async () => {
- await createComponent({}, { ahead: 0 });
-
- expect(findDivergenceMessage().text()).toBe('7 commits behind the upstream repository.');
- });
+ describe.each([
+ {
+ ahead: 7,
+ behind: 3,
+ message: '3 commits behind, 7 commits ahead of the upstream repository.',
+ firstLink: propsForkInfo.behindComparePath,
+ secondLink: propsForkInfo.aheadComparePath,
+ },
+ {
+ ahead: 7,
+ behind: 0,
+ message: '7 commits ahead of the upstream repository.',
+ firstLink: propsForkInfo.aheadComparePath,
+ secondLink: '',
+ },
+ {
+ ahead: 0,
+ behind: 3,
+ message: '3 commits behind the upstream repository.',
+ firstLink: propsForkInfo.behindComparePath,
+ secondLink: '',
+ },
+ ])(
+ 'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
+ ({ ahead, behind, message, firstLink, secondLink }) => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead, behind });
+ });
+
+ it('displays correct text', () => {
+ expect(findDivergenceMessage().text()).toBe(message);
+ });
+
+ it('adds correct links', () => {
+ const links = findCompareLinks();
+ expect(links.at(0).attributes('href')).toBe(firstLink);
+
+ if (secondLink) {
+ expect(links.at(1).attributes('href')).toBe(secondLink);
+ }
+ });
+ },
+ );
it('renders alert with error message when request fails', async () => {
await createComponent({}, {}, true);
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 964b135bee3..7226e7baa36 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -20,7 +20,7 @@ const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCommitRowDescription = () => wrapper.find('.commit-row-description');
-const findStatusBox = () => wrapper.find('.gpg-status-box');
+const findStatusBox = () => wrapper.find('.signature-badge');
const findItemTitle = () => wrapper.find('.item-title');
const defaultPipelineEdges = [
@@ -206,7 +206,7 @@ describe('Repository last commit component', () => {
it('renders the signature HTML as returned by the backend', async () => {
createComponent({
signatureHtml: `<a
- class="btn gpg-status-box valid"
+ class="btn signature-badge"
data-content="signature-content"
data-html="true"
data-placement="top"
@@ -214,12 +214,12 @@ describe('Repository last commit component', () => {
data-toggle="popover"
role="button"
tabindex="0"
- >Verified</a>`,
+ ><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
});
await waitForPromises();
expect(findStatusBox().html()).toBe(
- `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">Verified</a>`,
+ `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0"><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
);
});
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index e4eba65795e..d4c746b67d6 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -9,9 +9,14 @@ jest.mock('~/lib/utils/common_utils');
let vm;
let $apollo;
-function factory(blob) {
+function factory(blob, loading) {
$apollo = {
- query: jest.fn().mockReturnValue(Promise.resolve({})),
+ queries: {
+ readme: {
+ query: jest.fn().mockReturnValue(Promise.resolve({})),
+ loading,
+ },
+ },
};
vm = shallowMount(Preview, {
@@ -58,14 +63,13 @@ describe('Repository file preview component', () => {
});
it('renders loading icon', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ loading: 1 });
+ factory(
+ {
+ webPath: 'http://test.com',
+ name: 'README.md',
+ },
+ true,
+ );
await nextTick();
expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index c1309539b6d..a2e86c86add 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'helpers/mock_apollo_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolveCommit, fetchLogsTree } from '~/repository/log_tree';
import commitsQuery from '~/repository/queries/commits.query.graphql';
import projectPathQuery from '~/repository/queries/project_path.query.graphql';
@@ -47,7 +48,7 @@ describe('fetchLogsTree', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(/(.*)/).reply(200, mockData, {});
+ mock.onGet(/(.*)/).reply(HTTP_STATUS_OK, mockData, {});
jest.spyOn(axios, 'get');
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
new file mode 100644
index 00000000000..7c48fe440d2
--- /dev/null
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import highlightMixin from '~/repository/mixins/highlight_mixin';
+import LineHighlighter from '~/blob/line_highlighter';
+import Tracking from '~/tracking';
+import { TEXT_FILE_TYPE } from '~/repository/constants';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_FALLBACK,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
+jest.mock('~/vue_shared/components/source_viewer/workers/highlight_utils', () => ({
+ splitIntoChunks: jest.fn().mockResolvedValue([]),
+}));
+
+const workerMock = { postMessage: jest.fn() };
+const onErrorMock = jest.fn();
+
+describe('HighlightMixin', () => {
+ let wrapper;
+ const hash = '#L50';
+ const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
+ const rawTextBlob = contentArray.join('\n');
+ const languageMock = 'javascript';
+
+ const createComponent = ({ fileType = TEXT_FILE_TYPE, language = languageMock } = {}) => {
+ const simpleViewer = { fileType };
+
+ const dummyComponent = {
+ mixins: [highlightMixin],
+ inject: { highlightWorker: { default: workerMock } },
+ template: '<div>{{chunks[0]?.highlightedContent}}</div>',
+ created() {
+ this.initHighlightWorker({ rawTextBlob, simpleViewer, language });
+ },
+ methods: { onError: onErrorMock },
+ };
+
+ wrapper = shallowMount(dummyComponent, { mocks: { $route: { hash } } });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ describe('initHighlightWorker', () => {
+ const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
+
+ it('does not instruct worker if file is not a text file', () => {
+ workerMock.postMessage.mockClear();
+ createComponent({ fileType: 'markdown' });
+
+ expect(workerMock.postMessage).not.toHaveBeenCalled();
+ });
+
+ it('tracks event if a language is not supported and does not instruct worker', () => {
+ const unsupportedLanguage = 'some_unsupported_language';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+
+ jest.spyOn(Tracking, 'event');
+ workerMock.postMessage.mockClear();
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(onErrorMock).toHaveBeenCalled();
+ expect(workerMock.postMessage).not.toHaveBeenCalled();
+ });
+
+ it('generates a chunk for the first 70 lines of raw text', () => {
+ expect(splitIntoChunks).toHaveBeenCalledWith(languageMock, firstSeventyLines);
+ });
+
+ it('calls postMessage on the worker', () => {
+ expect(workerMock.postMessage.mock.calls.length).toBe(2);
+
+ // first call instructs worker to highlight the first 70 lines
+ expect(workerMock.postMessage.mock.calls[0][0]).toMatchObject({
+ content: firstSeventyLines,
+ language: languageMock,
+ });
+
+ // second call instructs worker to highlight all of the lines
+ expect(workerMock.postMessage.mock.calls[1][0]).toMatchObject({
+ content: rawTextBlob,
+ language: languageMock,
+ });
+ });
+ });
+
+ describe('worker message handling', () => {
+ const CHUNK_MOCK = { startingFrom: 0, totalLines: 70, highlightedContent: 'some content' };
+
+ beforeEach(() => workerMock.onmessage({ data: [CHUNK_MOCK] }));
+
+ it('updates the chunks data', () => {
+ expect(wrapper.text()).toBe(CHUNK_MOCK.highlightedContent);
+ });
+
+ it('highlights hash', () => {
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
+});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index d85434a9148..04ffe52bc3f 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -120,7 +120,9 @@ export const graphQLErrors = [
export const propsForkInfo = {
projectPath: 'nataliia/myGitLab',
- selectedRef: 'main',
+ selectedBranch: 'main',
sourceName: 'gitLab',
sourcePath: 'gitlab-org/gitlab',
+ aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
+ behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
};
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
index 4d0250fffbf..7f708f13eaa 100644
--- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -18,12 +18,14 @@ describe('generateRefDestinationPath', () => {
${`${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);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(result);
});
it('encodes the selected ref', () => {
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
- expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
+ result,
+ );
});
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index 3b220ba8351..f51d51ee182 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -69,6 +69,9 @@ describe('RightSidebar', () => {
});
it('should not hide collapsed icons', () => {
+ $toggle.click();
+ assertSidebarState('collapsed');
+
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBe(false);
});
diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
new file mode 100644
index 00000000000..3abdfcdaf20
--- /dev/null
+++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Saved replies list item component renders list item 1`] = `
+<li
+ class="gl-mb-5"
+>
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <strong>
+ test
+ </strong>
+ </div>
+
+ <div
+ class="gl-mt-3 gl-font-monospace"
+ >
+ /assign_reviewer
+ </div>
+</li>
+`;
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
new file mode 100644
index 00000000000..cad1000473b
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_item_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import ListItem from '~/saved_replies/components/list_item.vue';
+
+let wrapper;
+
+function createComponent(propsData = {}) {
+ return shallowMount(ListItem, {
+ propsData,
+ });
+}
+
+describe('Saved replies list item component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders list item', async () => {
+ wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js
new file mode 100644
index 00000000000..66e9ddfe148
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import List from '~/saved_replies/components/list.vue';
+import ListItem from '~/saved_replies/components/list_item.vue';
+import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
+
+let wrapper;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mount(List, {
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Saved replies list component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not render any list items when response is empty', async () => {
+ const mockApollo = createMockApolloProvider(noSavedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(0);
+ });
+
+ it('render saved replies count', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
+ });
+
+ it('renders list of saved replies', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index e02d3b0eab8..fb9c0a93907 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -192,3 +192,464 @@ export const MOCK_NAVIGATION_ACTION_MUTATION = {
type: types.RECEIVE_NAVIGATION_COUNT,
payload: { key: 'projects', count: '13' },
};
+
+export const MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: [
+ { key: 'random-label-edumingos0', count: 1 },
+ { key: 'random-label-rbourgourd1', count: 2 },
+ { key: 'random-label-dfearnside2', count: 3 },
+ { key: 'random-label-gewins3', count: 4 },
+ { key: 'random-label-telverstone4', count: 5 },
+ { key: 'random-label-ygerriets5', count: 6 },
+ { key: 'random-label-lmoffet6', count: 7 },
+ { key: 'random-label-ehinnerk7', count: 8 },
+ { key: 'random-label-flanceley8', count: 9 },
+ { key: 'random-label-adoyle9', count: 10 },
+ { key: 'random-label-rmcgirla', count: 11 },
+ { key: 'random-label-dwhellansb', count: 12 },
+ { key: 'random-label-apitkethlyc', count: 13 },
+ { key: 'random-label-senevoldsend', count: 14 },
+ { key: 'random-label-tlardnare', count: 15 },
+ { key: 'random-label-fcoilsf', count: 16 },
+ { key: 'random-label-qgeckg', count: 17 },
+ { key: 'random-label-rgrabenh', count: 18 },
+ { key: 'random-label-lashardi', count: 19 },
+ { key: 'random-label-sadamovitchj', count: 20 },
+ { key: 'random-label-rlyddiardk', count: 21 },
+ { key: 'random-label-jpoell', count: 22 },
+ { key: 'random-label-kcharitym', count: 23 },
+ { key: 'random-label-cbertenshawn', count: 24 },
+ { key: 'random-label-jsturgeso', count: 25 },
+ { key: 'random-label-ohouldcroftp', count: 26 },
+ { key: 'random-label-rheijnenq', count: 27 },
+ { key: 'random-label-snortheyr', count: 28 },
+ { key: 'random-label-vpairpoints', count: 29 },
+ { key: 'random-label-odavidovicit', count: 30 },
+ { key: 'random-label-fmccartu', count: 31 },
+ { key: 'random-label-cwansburyv', count: 32 },
+ { key: 'random-label-bdimontw', count: 33 },
+ { key: 'random-label-adocketx', count: 34 },
+ { key: 'random-label-obavridgey', count: 35 },
+ { key: 'random-label-jperezz', count: 36 },
+ { key: 'random-label-gdeneve10', count: 37 },
+ { key: 'random-label-rmckeand11', count: 38 },
+ { key: 'random-label-kwestmerland12', count: 39 },
+ { key: 'random-label-mpryer13', count: 40 },
+ { key: 'random-label-rmcneil14', count: 41 },
+ { key: 'random-label-ablondel15', count: 42 },
+ { key: 'random-label-wbalducci16', count: 43 },
+ { key: 'random-label-swigley17', count: 44 },
+ { key: 'random-label-gferroni18', count: 45 },
+ { key: 'random-label-icollings19', count: 46 },
+ { key: 'random-label-wszymanski1a', count: 47 },
+ { key: 'random-label-jelson1b', count: 48 },
+ { key: 'random-label-fsambrook1c', count: 49 },
+ { key: 'random-label-kconey1d', count: 50 },
+ { key: 'random-label-agoodread1e', count: 51 },
+ { key: 'random-label-nmewton1f', count: 52 },
+ { key: 'random-label-gcodman1g', count: 53 },
+ { key: 'random-label-rpoplee1h', count: 54 },
+ { key: 'random-label-mhug1i', count: 55 },
+ { key: 'random-label-ggowrie1j', count: 56 },
+ { key: 'random-label-ctonepohl1k', count: 57 },
+ { key: 'random-label-cstillman1l', count: 58 },
+ { key: 'random-label-dcollyer1m', count: 59 },
+ { key: 'random-label-idimelow1n', count: 60 },
+ { key: 'random-label-djarley1o', count: 61 },
+ { key: 'random-label-omclleese1p', count: 62 },
+ { key: 'random-label-dstivers1q', count: 63 },
+ { key: 'random-label-svose1r', count: 64 },
+ { key: 'random-label-clanfare1s', count: 65 },
+ { key: 'random-label-aport1t', count: 66 },
+ { key: 'random-label-hcarlett1u', count: 67 },
+ { key: 'random-label-dstillmann1v', count: 68 },
+ { key: 'random-label-ncorpe1w', count: 69 },
+ { key: 'random-label-mjacobsohn1x', count: 70 },
+ { key: 'random-label-ycleiment1y', count: 71 },
+ { key: 'random-label-owherton1z', count: 72 },
+ { key: 'random-label-anowaczyk20', count: 73 },
+ { key: 'random-label-rmckennan21', count: 74 },
+ { key: 'random-label-cmoulding22', count: 75 },
+ { key: 'random-label-sswate23', count: 76 },
+ { key: 'random-label-cbarge24', count: 77 },
+ { key: 'random-label-agrainger25', count: 78 },
+ { key: 'random-label-ncosin26', count: 79 },
+ { key: 'random-label-pkears27', count: 80 },
+ { key: 'random-label-cmcarthur28', count: 81 },
+ { key: 'random-label-jmantripp29', count: 82 },
+ { key: 'random-label-cjekel2a', count: 83 },
+ { key: 'random-label-hdilleway2b', count: 84 },
+ { key: 'random-label-lbovaird2c', count: 85 },
+ { key: 'random-label-mweld2d', count: 86 },
+ { key: 'random-label-marnowitz2e', count: 87 },
+ { key: 'random-label-nbertomieu2f', count: 88 },
+ { key: 'random-label-mledward2g', count: 89 },
+ { key: 'random-label-mhince2h', count: 90 },
+ { key: 'random-label-baarons2i', count: 91 },
+ { key: 'random-label-kfrancie2j', count: 92 },
+ { key: 'random-label-ishooter2k', count: 93 },
+ { key: 'random-label-glowmass2l', count: 94 },
+ { key: 'random-label-rgeorgi2m', count: 95 },
+ { key: 'random-label-bproby2n', count: 96 },
+ { key: 'random-label-hsteffan2o', count: 97 },
+ { key: 'random-label-doruane2p', count: 98 },
+ { key: 'random-label-rlunny2q', count: 99 },
+ { key: 'random-label-geles2r', count: 100 },
+ { key: 'random-label-nmaggiore2s', count: 101 },
+ { key: 'random-label-aboocock2t', count: 102 },
+ { key: 'random-label-eguilbert2u', count: 103 },
+ { key: 'random-label-emccutcheon2v', count: 104 },
+ { key: 'random-label-hcowser2w', count: 105 },
+ { key: 'random-label-dspeeding2x', count: 106 },
+ { key: 'random-label-oseebright2y', count: 107 },
+ { key: 'random-label-hpresdee2z', count: 108 },
+ { key: 'random-label-pesseby30', count: 109 },
+ { key: 'random-label-hpusey31', count: 110 },
+ { key: 'random-label-dmanthorpe32', count: 111 },
+ { key: 'random-label-natley33', count: 112 },
+ { key: 'random-label-iferentz34', count: 113 },
+ { key: 'random-label-adyble35', count: 114 },
+ { key: 'random-label-dlockitt36', count: 115 },
+ { key: 'random-label-acoxwell37', count: 116 },
+ { key: 'random-label-amcgarvey38', count: 117 },
+ { key: 'random-label-rmcgougan39', count: 118 },
+ { key: 'random-label-mscole3a', count: 119 },
+ { key: 'random-label-lmalim3b', count: 120 },
+ { key: 'random-label-cends3c', count: 121 },
+ { key: 'random-label-dmannie3d', count: 122 },
+ { key: 'random-label-lgoodricke3e', count: 123 },
+ { key: 'random-label-rcaghy3f', count: 124 },
+ { key: 'random-label-mprozillo3g', count: 125 },
+ { key: 'random-label-mcardnell3h', count: 126 },
+ { key: 'random-label-gericssen3i', count: 127 },
+ { key: 'random-label-fspooner3j', count: 128 },
+ { key: 'random-label-achadney3k', count: 129 },
+ { key: 'random-label-corchard3l', count: 130 },
+ { key: 'random-label-lyerill3m', count: 131 },
+ { key: 'random-label-jrusk3n', count: 132 },
+ { key: 'random-label-lbonelle3o', count: 133 },
+ { key: 'random-label-eduny3p', count: 134 },
+ { key: 'random-label-mhutchence3q', count: 135 },
+ { key: 'random-label-rmargeram3r', count: 136 },
+ { key: 'random-label-smaudlin3s', count: 137 },
+ { key: 'random-label-sfarrance3t', count: 138 },
+ { key: 'random-label-eclendennen3u', count: 139 },
+ { key: 'random-label-cyabsley3v', count: 140 },
+ { key: 'random-label-ahensmans3w', count: 141 },
+ { key: 'random-label-tsenchenko3x', count: 142 },
+ { key: 'random-label-ryurchishin3y', count: 143 },
+ { key: 'random-label-teby3z', count: 144 },
+ { key: 'random-label-dvaillant40', count: 145 },
+ { key: 'random-label-kpetyakov41', count: 146 },
+ { key: 'random-label-cmorrison42', count: 147 },
+ { key: 'random-label-ltwiddy43', count: 148 },
+ { key: 'random-label-ineame44', count: 149 },
+ { key: 'random-label-blucock45', count: 150 },
+ { key: 'random-label-kdunsford46', count: 151 },
+ { key: 'random-label-dducham47', count: 152 },
+ { key: 'random-label-javramovitz48', count: 153 },
+ { key: 'random-label-mascraft49', count: 154 },
+ { key: 'random-label-bloughead4a', count: 155 },
+ { key: 'random-label-sduckit4b', count: 156 },
+ { key: 'random-label-hhardman4c', count: 157 },
+ { key: 'random-label-cstaniforth4d', count: 158 },
+ { key: 'random-label-jedney4e', count: 159 },
+ { key: 'random-label-bobbard4f', count: 160 },
+ { key: 'random-label-cgiraux4g', count: 161 },
+ { key: 'random-label-tkiln4h', count: 162 },
+ { key: 'random-label-jwansbury4i', count: 163 },
+ { key: 'random-label-dquinlan4j', count: 164 },
+ { key: 'random-label-hgindghill4k', count: 165 },
+ { key: 'random-label-jjowle4l', count: 166 },
+ { key: 'random-label-egambrell4m', count: 167 },
+ { key: 'random-label-jmcgloughlin4n', count: 168 },
+ { key: 'random-label-bbabb4o', count: 169 },
+ { key: 'random-label-achuck4p', count: 170 },
+ { key: 'random-label-tsyers4q', count: 171 },
+ { key: 'random-label-jlandon4r', count: 172 },
+ { key: 'random-label-wteather4s', count: 173 },
+ { key: 'random-label-dfoskin4t', count: 174 },
+ { key: 'random-label-gmorlon4u', count: 175 },
+ { key: 'random-label-jseely4v', count: 176 },
+ { key: 'random-label-cbrass4w', count: 177 },
+ { key: 'random-label-fmanilo4x', count: 178 },
+ { key: 'random-label-bfrangleton4y', count: 179 },
+ { key: 'random-label-vbartkiewicz4z', count: 180 },
+ { key: 'random-label-tclymer50', count: 181 },
+ { key: 'random-label-pqueen51', count: 182 },
+ { key: 'random-label-bpol52', count: 183 },
+ { key: 'random-label-jclaeskens53', count: 184 },
+ { key: 'random-label-cstranieri54', count: 185 },
+ { key: 'random-label-drumbelow55', count: 186 },
+ { key: 'random-label-wbrumham56', count: 187 },
+ { key: 'random-label-azeal57', count: 188 },
+ { key: 'random-label-msnooks58', count: 189 },
+ { key: 'random-label-blapre59', count: 190 },
+ { key: 'random-label-cduckers5a', count: 191 },
+ { key: 'random-label-mgumary5b', count: 192 },
+ { key: 'random-label-rtebbs5c', count: 193 },
+ { key: 'random-label-eroe5d', count: 194 },
+ { key: 'random-label-rconfait5e', count: 195 },
+ { key: 'random-label-fsinderland5f', count: 196 },
+ { key: 'random-label-tdallywater5g', count: 197 },
+ { key: 'random-label-glindenman5h', count: 198 },
+ { key: 'random-label-fbauser5i', count: 199 },
+ { key: 'random-label-bdownton5j', count: 200 },
+ ],
+ },
+];
+
+export const MOCK_LANGUAGE_AGGREGATIONS_BUCKETS = [
+ { key: 'random-label-edumingos0', count: 1 },
+ { key: 'random-label-rbourgourd1', count: 2 },
+ { key: 'random-label-dfearnside2', count: 3 },
+ { key: 'random-label-gewins3', count: 4 },
+ { key: 'random-label-telverstone4', count: 5 },
+ { key: 'random-label-ygerriets5', count: 6 },
+ { key: 'random-label-lmoffet6', count: 7 },
+ { key: 'random-label-ehinnerk7', count: 8 },
+ { key: 'random-label-flanceley8', count: 9 },
+ { key: 'random-label-adoyle9', count: 10 },
+ { key: 'random-label-rmcgirla', count: 11 },
+ { key: 'random-label-dwhellansb', count: 12 },
+ { key: 'random-label-apitkethlyc', count: 13 },
+ { key: 'random-label-senevoldsend', count: 14 },
+ { key: 'random-label-tlardnare', count: 15 },
+ { key: 'random-label-fcoilsf', count: 16 },
+ { key: 'random-label-qgeckg', count: 17 },
+ { key: 'random-label-rgrabenh', count: 18 },
+ { key: 'random-label-lashardi', count: 19 },
+ { key: 'random-label-sadamovitchj', count: 20 },
+ { key: 'random-label-rlyddiardk', count: 21 },
+ { key: 'random-label-jpoell', count: 22 },
+ { key: 'random-label-kcharitym', count: 23 },
+ { key: 'random-label-cbertenshawn', count: 24 },
+ { key: 'random-label-jsturgeso', count: 25 },
+ { key: 'random-label-ohouldcroftp', count: 26 },
+ { key: 'random-label-rheijnenq', count: 27 },
+ { key: 'random-label-snortheyr', count: 28 },
+ { key: 'random-label-vpairpoints', count: 29 },
+ { key: 'random-label-odavidovicit', count: 30 },
+ { key: 'random-label-fmccartu', count: 31 },
+ { key: 'random-label-cwansburyv', count: 32 },
+ { key: 'random-label-bdimontw', count: 33 },
+ { key: 'random-label-adocketx', count: 34 },
+ { key: 'random-label-obavridgey', count: 35 },
+ { key: 'random-label-jperezz', count: 36 },
+ { key: 'random-label-gdeneve10', count: 37 },
+ { key: 'random-label-rmckeand11', count: 38 },
+ { key: 'random-label-kwestmerland12', count: 39 },
+ { key: 'random-label-mpryer13', count: 40 },
+ { key: 'random-label-rmcneil14', count: 41 },
+ { key: 'random-label-ablondel15', count: 42 },
+ { key: 'random-label-wbalducci16', count: 43 },
+ { key: 'random-label-swigley17', count: 44 },
+ { key: 'random-label-gferroni18', count: 45 },
+ { key: 'random-label-icollings19', count: 46 },
+ { key: 'random-label-wszymanski1a', count: 47 },
+ { key: 'random-label-jelson1b', count: 48 },
+ { key: 'random-label-fsambrook1c', count: 49 },
+ { key: 'random-label-kconey1d', count: 50 },
+ { key: 'random-label-agoodread1e', count: 51 },
+ { key: 'random-label-nmewton1f', count: 52 },
+ { key: 'random-label-gcodman1g', count: 53 },
+ { key: 'random-label-rpoplee1h', count: 54 },
+ { key: 'random-label-mhug1i', count: 55 },
+ { key: 'random-label-ggowrie1j', count: 56 },
+ { key: 'random-label-ctonepohl1k', count: 57 },
+ { key: 'random-label-cstillman1l', count: 58 },
+ { key: 'random-label-dcollyer1m', count: 59 },
+ { key: 'random-label-idimelow1n', count: 60 },
+ { key: 'random-label-djarley1o', count: 61 },
+ { key: 'random-label-omclleese1p', count: 62 },
+ { key: 'random-label-dstivers1q', count: 63 },
+ { key: 'random-label-svose1r', count: 64 },
+ { key: 'random-label-clanfare1s', count: 65 },
+ { key: 'random-label-aport1t', count: 66 },
+ { key: 'random-label-hcarlett1u', count: 67 },
+ { key: 'random-label-dstillmann1v', count: 68 },
+ { key: 'random-label-ncorpe1w', count: 69 },
+ { key: 'random-label-mjacobsohn1x', count: 70 },
+ { key: 'random-label-ycleiment1y', count: 71 },
+ { key: 'random-label-owherton1z', count: 72 },
+ { key: 'random-label-anowaczyk20', count: 73 },
+ { key: 'random-label-rmckennan21', count: 74 },
+ { key: 'random-label-cmoulding22', count: 75 },
+ { key: 'random-label-sswate23', count: 76 },
+ { key: 'random-label-cbarge24', count: 77 },
+ { key: 'random-label-agrainger25', count: 78 },
+ { key: 'random-label-ncosin26', count: 79 },
+ { key: 'random-label-pkears27', count: 80 },
+ { key: 'random-label-cmcarthur28', count: 81 },
+ { key: 'random-label-jmantripp29', count: 82 },
+ { key: 'random-label-cjekel2a', count: 83 },
+ { key: 'random-label-hdilleway2b', count: 84 },
+ { key: 'random-label-lbovaird2c', count: 85 },
+ { key: 'random-label-mweld2d', count: 86 },
+ { key: 'random-label-marnowitz2e', count: 87 },
+ { key: 'random-label-nbertomieu2f', count: 88 },
+ { key: 'random-label-mledward2g', count: 89 },
+ { key: 'random-label-mhince2h', count: 90 },
+ { key: 'random-label-baarons2i', count: 91 },
+ { key: 'random-label-kfrancie2j', count: 92 },
+ { key: 'random-label-ishooter2k', count: 93 },
+ { key: 'random-label-glowmass2l', count: 94 },
+ { key: 'random-label-rgeorgi2m', count: 95 },
+ { key: 'random-label-bproby2n', count: 96 },
+ { key: 'random-label-hsteffan2o', count: 97 },
+ { key: 'random-label-doruane2p', count: 98 },
+ { key: 'random-label-rlunny2q', count: 99 },
+ { key: 'random-label-geles2r', count: 100 },
+ { key: 'random-label-nmaggiore2s', count: 101 },
+ { key: 'random-label-aboocock2t', count: 102 },
+ { key: 'random-label-eguilbert2u', count: 103 },
+ { key: 'random-label-emccutcheon2v', count: 104 },
+ { key: 'random-label-hcowser2w', count: 105 },
+ { key: 'random-label-dspeeding2x', count: 106 },
+ { key: 'random-label-oseebright2y', count: 107 },
+ { key: 'random-label-hpresdee2z', count: 108 },
+ { key: 'random-label-pesseby30', count: 109 },
+ { key: 'random-label-hpusey31', count: 110 },
+ { key: 'random-label-dmanthorpe32', count: 111 },
+ { key: 'random-label-natley33', count: 112 },
+ { key: 'random-label-iferentz34', count: 113 },
+ { key: 'random-label-adyble35', count: 114 },
+ { key: 'random-label-dlockitt36', count: 115 },
+ { key: 'random-label-acoxwell37', count: 116 },
+ { key: 'random-label-amcgarvey38', count: 117 },
+ { key: 'random-label-rmcgougan39', count: 118 },
+ { key: 'random-label-mscole3a', count: 119 },
+ { key: 'random-label-lmalim3b', count: 120 },
+ { key: 'random-label-cends3c', count: 121 },
+ { key: 'random-label-dmannie3d', count: 122 },
+ { key: 'random-label-lgoodricke3e', count: 123 },
+ { key: 'random-label-rcaghy3f', count: 124 },
+ { key: 'random-label-mprozillo3g', count: 125 },
+ { key: 'random-label-mcardnell3h', count: 126 },
+ { key: 'random-label-gericssen3i', count: 127 },
+ { key: 'random-label-fspooner3j', count: 128 },
+ { key: 'random-label-achadney3k', count: 129 },
+ { key: 'random-label-corchard3l', count: 130 },
+ { key: 'random-label-lyerill3m', count: 131 },
+ { key: 'random-label-jrusk3n', count: 132 },
+ { key: 'random-label-lbonelle3o', count: 133 },
+ { key: 'random-label-eduny3p', count: 134 },
+ { key: 'random-label-mhutchence3q', count: 135 },
+ { key: 'random-label-rmargeram3r', count: 136 },
+ { key: 'random-label-smaudlin3s', count: 137 },
+ { key: 'random-label-sfarrance3t', count: 138 },
+ { key: 'random-label-eclendennen3u', count: 139 },
+ { key: 'random-label-cyabsley3v', count: 140 },
+ { key: 'random-label-ahensmans3w', count: 141 },
+ { key: 'random-label-tsenchenko3x', count: 142 },
+ { key: 'random-label-ryurchishin3y', count: 143 },
+ { key: 'random-label-teby3z', count: 144 },
+ { key: 'random-label-dvaillant40', count: 145 },
+ { key: 'random-label-kpetyakov41', count: 146 },
+ { key: 'random-label-cmorrison42', count: 147 },
+ { key: 'random-label-ltwiddy43', count: 148 },
+ { key: 'random-label-ineame44', count: 149 },
+ { key: 'random-label-blucock45', count: 150 },
+ { key: 'random-label-kdunsford46', count: 151 },
+ { key: 'random-label-dducham47', count: 152 },
+ { key: 'random-label-javramovitz48', count: 153 },
+ { key: 'random-label-mascraft49', count: 154 },
+ { key: 'random-label-bloughead4a', count: 155 },
+ { key: 'random-label-sduckit4b', count: 156 },
+ { key: 'random-label-hhardman4c', count: 157 },
+ { key: 'random-label-cstaniforth4d', count: 158 },
+ { key: 'random-label-jedney4e', count: 159 },
+ { key: 'random-label-bobbard4f', count: 160 },
+ { key: 'random-label-cgiraux4g', count: 161 },
+ { key: 'random-label-tkiln4h', count: 162 },
+ { key: 'random-label-jwansbury4i', count: 163 },
+ { key: 'random-label-dquinlan4j', count: 164 },
+ { key: 'random-label-hgindghill4k', count: 165 },
+ { key: 'random-label-jjowle4l', count: 166 },
+ { key: 'random-label-egambrell4m', count: 167 },
+ { key: 'random-label-jmcgloughlin4n', count: 168 },
+ { key: 'random-label-bbabb4o', count: 169 },
+ { key: 'random-label-achuck4p', count: 170 },
+ { key: 'random-label-tsyers4q', count: 171 },
+ { key: 'random-label-jlandon4r', count: 172 },
+ { key: 'random-label-wteather4s', count: 173 },
+ { key: 'random-label-dfoskin4t', count: 174 },
+ { key: 'random-label-gmorlon4u', count: 175 },
+ { key: 'random-label-jseely4v', count: 176 },
+ { key: 'random-label-cbrass4w', count: 177 },
+ { key: 'random-label-fmanilo4x', count: 178 },
+ { key: 'random-label-bfrangleton4y', count: 179 },
+ { key: 'random-label-vbartkiewicz4z', count: 180 },
+ { key: 'random-label-tclymer50', count: 181 },
+ { key: 'random-label-pqueen51', count: 182 },
+ { key: 'random-label-bpol52', count: 183 },
+ { key: 'random-label-jclaeskens53', count: 184 },
+ { key: 'random-label-cstranieri54', count: 185 },
+ { key: 'random-label-drumbelow55', count: 186 },
+ { key: 'random-label-wbrumham56', count: 187 },
+ { key: 'random-label-azeal57', count: 188 },
+ { key: 'random-label-msnooks58', count: 189 },
+ { key: 'random-label-blapre59', count: 190 },
+ { key: 'random-label-cduckers5a', count: 191 },
+ { key: 'random-label-mgumary5b', count: 192 },
+ { key: 'random-label-rtebbs5c', count: 193 },
+ { key: 'random-label-eroe5d', count: 194 },
+ { key: 'random-label-rconfait5e', count: 195 },
+ { key: 'random-label-fsinderland5f', count: 196 },
+ { key: 'random-label-tdallywater5g', count: 197 },
+ { key: 'random-label-glindenman5h', count: 198 },
+ { key: 'random-label-fbauser5i', count: 199 },
+ { key: 'random-label-bdownton5j', count: 200 },
+];
+
+export const MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION = [
+ {
+ type: types.REQUEST_AGGREGATIONS,
+ },
+ {
+ type: types.RECEIVE_AGGREGATIONS_SUCCESS,
+ payload: MOCK_AGGREGATIONS,
+ },
+];
+
+export const MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION = [
+ {
+ type: types.REQUEST_AGGREGATIONS,
+ },
+ {
+ type: types.RECEIVE_AGGREGATIONS_ERROR,
+ },
+];
+
+export const TEST_RAW_BUCKETS = [
+ { key: 'Go', count: 350 },
+ { key: 'C', count: 298 },
+ { key: 'JavaScript', count: 128 },
+ { key: 'YAML', count: 58 },
+ { key: 'Text', count: 46 },
+ { key: 'Markdown', count: 37 },
+ { key: 'HTML', count: 34 },
+ { key: 'Shell', count: 34 },
+ { key: 'Makefile', count: 21 },
+ { key: 'JSON', count: 15 },
+];
+
+export const TEST_FILTER_DATA = {
+ header: 'Language',
+ scopes: { BLOBS: 'blobs' },
+ filterParam: 'language',
+ filters: {
+ GO: { label: 'Go', value: 'Go', count: 350 },
+ C: { label: 'C', value: 'C', count: 298 },
+ JAVASCRIPT: { label: 'JavaScript', value: 'JavaScript', count: 128 },
+ YAML: { label: 'YAML', value: 'YAML', count: 58 },
+ TEXT: { label: 'Text', value: 'Text', count: 46 },
+ MARKDOWN: { label: 'Markdown', value: 'Markdown', count: 37 },
+ HTML: { label: 'HTML', value: 'HTML', count: 34 },
+ SHELL: { label: 'Shell', value: 'Shell', count: 34 },
+ MAKEFILE: { label: 'Makefile', value: 'Makefile', count: 21 },
+ JSON: { label: 'JSON', value: 'JSON', count: 15 },
+ },
+};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index e87217950cd..83302b90233 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -5,6 +5,7 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
+import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
Vue.use(Vuex);
@@ -35,72 +36,66 @@ describe('GlobalSearchSidebar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSidebarSection = () => wrapper.find('section');
const findFilters = () => wrapper.findComponent(ResultsFilters);
const findSidebarNavigation = () => wrapper.findComponent(ScopeNavigation);
+ const findLanguageAggregation = () => wrapper.findComponent(LanguageFilter);
describe('renders properly', () => {
- describe('scope=projects', () => {
+ describe('always', () => {
beforeEach(() => {
- createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
+ createComponent({});
});
-
- it('shows section', () => {
+ it(`shows section`, () => {
expect(findSidebarSection().exists()).toBe(true);
});
-
- it("doesn't shows filters", () => {
- expect(findFilters().exists()).toBe(false);
- });
});
- describe('scope=merge_requests', () => {
+ describe.each`
+ scope | showFilters | ShowsLanguage
+ ${'issues'} | ${true} | ${false}
+ ${'merge_requests'} | ${true} | ${false}
+ ${'projects'} | ${false} | ${false}
+ ${'blobs'} | ${false} | ${true}
+ `('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => {
beforeEach(() => {
- createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } });
+ createComponent({ urlQuery: { scope } }, { searchBlobsLanguageAggregation: true });
});
- it('shows section', () => {
- expect(findSidebarSection().exists()).toBe(true);
+ it(`${!showFilters ? "doesn't" : ''} shows filters`, () => {
+ expect(findFilters().exists()).toBe(showFilters);
});
- it('shows filters', () => {
- expect(findFilters().exists()).toBe(true);
+ it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => {
+ expect(findLanguageAggregation().exists()).toBe(ShowsLanguage);
});
});
- describe('scope=issues', () => {
+ describe('renders navigation', () => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
- it('shows section', () => {
- expect(findSidebarSection().exists()).toBe(true);
+ createComponent({});
});
-
- it('shows filters', () => {
- expect(findFilters().exists()).toBe(true);
+ it('shows the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(true);
});
});
});
- describe('when search_page_vertical_nav is enabled', () => {
+ describe('when search_blobs_language_aggregation is enabled', () => {
beforeEach(() => {
- createComponent({}, { searchPageVerticalNav: true });
+ createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true });
});
- it('shows the vertical navigation', () => {
- expect(findSidebarNavigation().exists()).toBe(true);
+ it('shows the language filter', () => {
+ expect(findLanguageAggregation().exists()).toBe(true);
});
});
- describe('when search_page_vertical_nav is disabled', () => {
+ describe('when search_blobs_language_aggregation is disabled', () => {
beforeEach(() => {
- createComponent({}, { searchPageVerticalNav: false });
+ createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false });
});
- it('hides the vertical navigation', () => {
- expect(findSidebarNavigation().exists()).toBe(false);
+ it('hides the language filter', () => {
+ expect(findLanguageAggregation().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
new file mode 100644
index 00000000000..82017754b23
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -0,0 +1,85 @@
+import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { MOCK_QUERY, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS } from 'jest/search/mock_data';
+import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
+
+import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
+import { convertFiltersData } from '~/search/sidebar/utils';
+
+Vue.use(Vuex);
+
+describe('CheckboxFilter', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setQuery: jest.fn(),
+ };
+
+ const defaultProps = {
+ filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ };
+
+ const createComponent = () => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMountExtended(CheckboxFilter, {
+ store,
+ propsData: {
+ ...defaultProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findFormCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
+ const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
+ const fintAllCheckboxLabels = () => wrapper.findAllByTestId('label');
+ const fintAllCheckboxLabelCounts = () => wrapper.findAllByTestId('labelCount');
+
+ describe('Renders correctly', () => {
+ it('renders form', () => {
+ expect(findFormCheckboxGroup().exists()).toBe(true);
+ });
+
+ it('renders checkbox-filter', () => {
+ expect(findAllCheckboxes().exists()).toBe(true);
+ });
+
+ it('renders all checkbox-filter checkboxes', () => {
+ expect(findAllCheckboxes()).toHaveLength(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.length);
+ });
+
+ it('renders correctly label for the element', () => {
+ expect(fintAllCheckboxLabels().at(0).text()).toBe(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].key);
+ });
+
+ it('renders correctly count for the element', () => {
+ expect(fintAllCheckboxLabelCounts().at(0).text()).toBe(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].count.toString(),
+ );
+ });
+ });
+
+ describe('actions', () => {
+ it('triggers setQuery', () => {
+ const filter =
+ defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value;
+ findFormCheckboxGroup().vm.$emit('input', filter);
+
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: languageFilterData.filterParam,
+ value: filter,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index d5ecca4636c..4f146757454 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -22,24 +22,4 @@ describe('ConfidentialityFilter', () => {
expect(findRadioFilter().exists()).toBe(true);
});
});
-
- 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/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filters_spec.js
new file mode 100644
index 00000000000..e297d1c33b0
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/language_filters_spec.js
@@ -0,0 +1,152 @@
+import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ MOCK_QUERY,
+ MOCK_AGGREGATIONS,
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+} from 'jest/search/mock_data';
+import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
+import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
+import { MAX_ITEM_LENGTH } from '~/search/sidebar/constants/language_filter_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchSidebarLanguageFilter', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchLanguageAggregation: jest.fn(),
+ applyQuery: jest.fn(),
+ };
+
+ const getterSpies = {
+ langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ urlQuery: MOCK_QUERY,
+ aggregations: MOCK_AGGREGATIONS,
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: getterSpies,
+ });
+
+ wrapper = shallowMountExtended(LanguageFilter, {
+ store,
+ stubs: {
+ CheckboxFilter,
+ },
+ });
+ };
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
+ const findApplyButton = () => wrapper.findByTestId('apply-button');
+ const findShowMoreButton = () => wrapper.findByTestId('show-more-button');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
+ const findHasOverMax = () => wrapper.findByTestId('has-over-max-text');
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('renders checkbox-filter', () => {
+ expect(findCheckboxFilter().exists()).toBe(true);
+ });
+
+ it('renders all checkbox-filter checkboxes', () => {
+ // 11th checkbox is hidden
+ expect(findAllCheckboxes()).toHaveLength(10);
+ });
+
+ it('renders ApplyButton', () => {
+ expect(findApplyButton().exists()).toBe(true);
+ });
+
+ it('renders Show More button', () => {
+ expect(findShowMoreButton().exists()).toBe(true);
+ });
+
+ it("doesn't render Alert", () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('Show All button works', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it(`renders ${MAX_ITEM_LENGTH} amount of items`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findAllCheckboxes()).toHaveLength(MAX_ITEM_LENGTH);
+ });
+
+ it(`renders more then ${MAX_ITEM_LENGTH} text`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findHasOverMax().exists()).toBe(true);
+ });
+
+ it(`doesn't render show more button after click`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findShowMoreButton().exists()).toBe(false);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+
+ it('uses getter langugageAggregationBuckets', () => {
+ expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled();
+ });
+
+ it('uses action fetchLanguageAggregation', () => {
+ expect(actionSpies.fetchLanguageAggregation).toHaveBeenCalled();
+ });
+
+ it('clicking ApplyButton calls applyQuery', () => {
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 2ed199469e6..6704634ef36 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -22,24 +22,4 @@ describe('StatusFilter', () => {
expect(findRadioFilter().exists()).toBe(true);
});
});
-
- 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/utils_spec.js b/spec/frontend/search/sidebar/utils_spec.js
new file mode 100644
index 00000000000..652d32c836e
--- /dev/null
+++ b/spec/frontend/search/sidebar/utils_spec.js
@@ -0,0 +1,10 @@
+import { convertFiltersData } from '~/search/sidebar/utils';
+import { TEST_RAW_BUCKETS, TEST_FILTER_DATA } from '../mock_data';
+
+describe('Global Search sidebar utils', () => {
+ describe('convertFiltersData', () => {
+ it('converts raw buckets to array', () => {
+ expect(convertFiltersData(TEST_RAW_BUCKETS)).toStrictEqual(TEST_FILTER_DATA);
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 3d19b27ff86..2f87802dfe6 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -4,6 +4,7 @@ import Api from '~/api';
import { createAlert } from '~/flash';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
import {
@@ -27,6 +28,9 @@ import {
MOCK_NAVIGATION_DATA,
MOCK_NAVIGATION_ACTION_MUTATION,
MOCK_ENDPOINT_RESPONSE,
+ MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION,
+ MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION,
+ MOCK_AGGREGATIONS,
} from '../mock_data';
jest.mock('~/flash');
@@ -59,11 +63,11 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
+ action | axiosMock | type | expectedMutations | flashCallCount
+ ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
+ ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
+ ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
+ ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
@@ -80,11 +84,11 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
- ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
- ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
- ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
- ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
+ action | axiosMock | type | expectedMutations | flashCallCount
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
`('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
@@ -269,10 +273,10 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | scope | expectedMutations | errorLogs
- ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${'issues'} | ${[]} | ${1}
+ action | axiosMock | type | scope | expectedMutations | errorLogs
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${'issues'} | ${[]} | ${1}
`('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -295,4 +299,30 @@ describe('Global Search Store Actions', () => {
});
});
});
+
+ describe.each`
+ action | axiosMock | type | expectedMutations | errorLogs
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION} | ${0}
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onPut', code: 0 }} | ${'error'} | ${MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION} | ${1}
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION} | ${1}
+ `('fetchLanguageAggregation', ({ action, axiosMock, type, expectedMutations, errorLogs }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ if (axiosMock.method) {
+ mock[axiosMock.method]().reply(
+ axiosMock.code,
+ axiosMock.code === HTTP_STATUS_OK ? MOCK_AGGREGATIONS : [],
+ );
+ }
+ });
+
+ it(`should ${type === 'error' ? 'NOT ' : ''}dispatch ${
+ type === 'error' ? '' : 'the correct '
+ }mutations`, () => {
+ return testAction({ action, state, expectedMutations }).then(() => {
+ expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 081e6a986eb..818902ee720 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -1,7 +1,13 @@
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import * as getters from '~/search/store/getters';
import createState from '~/search/store/state';
-import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_GROUPS,
+ MOCK_PROJECTS,
+ MOCK_AGGREGATIONS,
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+} from '../mock_data';
describe('Global Search Store Getters', () => {
let state;
@@ -29,4 +35,16 @@ describe('Global Search Store Getters', () => {
expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS);
});
});
+
+ describe('langugageAggregationBuckets', () => {
+ beforeEach(() => {
+ state.aggregations.data = MOCK_AGGREGATIONS;
+ });
+
+ it('returns the correct data', () => {
+ expect(getters.langugageAggregationBuckets(state)).toStrictEqual(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ );
+ });
+ });
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index a79ec8f70b0..d604cf38f8f 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -8,6 +8,7 @@ import {
MOCK_NAVIGATION_DATA,
MOCK_NAVIGATION_ACTION_MUTATION,
MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION,
+ MOCK_AGGREGATIONS,
} from '../mock_data';
describe('Global Search Store Mutations', () => {
@@ -108,4 +109,17 @@ describe('Global Search Store Mutations', () => {
);
});
});
+
+ describe.each`
+ mutation | data | result
+ ${types.REQUEST_AGGREGATIONS} | ${[]} | ${{ fetching: true, error: false, data: [] }}
+ ${types.RECEIVE_AGGREGATIONS_SUCCESS} | ${MOCK_AGGREGATIONS} | ${{ fetching: false, error: false, data: MOCK_AGGREGATIONS }}
+ ${types.RECEIVE_AGGREGATIONS_ERROR} | ${[]} | ${{ fetching: false, error: true, data: [] }}
+ `('$mutation', ({ mutation, data, result }) => {
+ it('sets correct object content', () => {
+ mutations[mutation](state, data);
+
+ expect(state.aggregations).toStrictEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 266f047e9dc..a3098fb81ea 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -5,6 +5,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import axios from '~/lib/utils/axios_utils';
import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Search autocomplete dropdown', () => {
let widget = null;
@@ -191,7 +192,7 @@ describe('Search autocomplete dropdown', () => {
const axiosMock = new AxiosMockAdapter(axios);
const autocompleteUrl = new RegExp(autocompletePath);
- axiosMock.onGet(autocompleteUrl).reply(200, [
+ axiosMock.onGet(autocompleteUrl).reply(HTTP_STATUS_OK, [
{
category: 'Projects',
id: 1,
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 4c266fabea6..0e28e330009 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -1,7 +1,11 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { HTTP_STATUS_ACCEPTED, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+} 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';
@@ -91,7 +95,7 @@ describe('self-monitor actions', () => {
describe('error', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
- mock.onPost(state.createProjectEndpoint).reply(500);
+ mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -198,7 +202,7 @@ describe('self-monitor actions', () => {
describe('error', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
- mock.onDelete(state.deleteProjectEndpoint).reply(500);
+ mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 4764f3607bc..60edab8766a 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
-import { TYPE_USER } from '~/graphql_shared/constants';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
@@ -133,7 +133,7 @@ describe('AssigneeAvatarLink component', () => {
createComponent({
tooltipHasName: true,
issuableType: 'issue',
- user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) },
+ user: { ...userDataMock(), id: convertToGraphQLId(TYPENAME_USER, userId) },
});
expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
index 71424aaead3..be0b14fa997 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -1,6 +1,6 @@
import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
const user = {
@@ -16,11 +16,7 @@ describe('Sidebar participant component', () => {
const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
const findIcon = () => wrapper.findComponent(GlIcon);
- const createComponent = ({
- status = null,
- issuableType = IssuableType.Issue,
- canMerge = false,
- } = {}) => {
+ const createComponent = ({ status = null, issuableType = TYPE_ISSUE, canMerge = false } = {}) => {
wrapper = shallowMount(SidebarParticipant, {
propsData: {
user: {
diff --git a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index 40f14d581dc..c3de076d6aa 100644
--- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
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';
@@ -19,7 +19,7 @@ describe('Sidebar Reference Widget', () => {
const findCopyableField = () => wrapper.findComponent(CopyableField);
const createComponent = ({
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
referenceQuery = issueReferenceQuery,
referenceQueryHandler = jest.fn().mockResolvedValue(issueReferenceResponse(mockReferenceValue)),
} = {}) => {
@@ -51,7 +51,7 @@ describe('Sidebar Reference Widget', () => {
});
describe.each([
- [IssuableType.Issue, issueReferenceQuery],
+ [TYPE_ISSUE, issueReferenceQuery],
[IssuableType.MergeRequest, mergeRequestReferenceQuery],
])('when issuableType is %s', (issuableType, referenceQuery) => {
it('sets CopyableField `value` prop to reference value', async () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index 0e0024aa6c2..55651bccaa8 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -3,6 +3,7 @@ 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
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';
@@ -121,7 +122,7 @@ describe('LabelsSelect Actions', () => {
describe('on success', () => {
it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- mock.onGet(/labels.json/).replyOnce(200, labels);
+ mock.onGet(/labels.json/).replyOnce(HTTP_STATUS_OK, labels);
return testAction(
actions.fetchLabels,
@@ -135,7 +136,7 @@ describe('LabelsSelect Actions', () => {
describe('on failure', () => {
it('dispatches `requestLabels` & `receiveLabelsFailure` actions', () => {
- mock.onGet(/labels.json/).replyOnce(500, {});
+ mock.onGet(/labels.json/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
actions.fetchLabels,
@@ -205,7 +206,7 @@ describe('LabelsSelect Actions', () => {
describe('on success', () => {
it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', () => {
const label = { id: 1 };
- mock.onPost(/labels.json/).replyOnce(200, label);
+ mock.onPost(/labels.json/).replyOnce(HTTP_STATUS_OK, label);
return testAction(
actions.createLabel,
@@ -224,7 +225,7 @@ describe('LabelsSelect Actions', () => {
describe('on failure', () => {
it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', () => {
- mock.onPost(/labels.json/).replyOnce(500, {});
+ mock.onPost(/labels.json/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
actions.createLabel,
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index 2995c268966..fd8e72bac49 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
@@ -14,6 +14,7 @@ import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutatio
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 '~/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import updateTestCaseLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import {
mockConfig,
@@ -34,9 +35,10 @@ const subscriptionHandler = jest.fn().mockResolvedValue(issuableLabelsSubscripti
const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const updateLabelsMutation = {
- [IssuableType.Issue]: updateIssueLabelsMutation,
+ [TYPE_ISSUE]: updateIssueLabelsMutation,
[IssuableType.MergeRequest]: updateMergeRequestLabelsMutation,
- [IssuableType.Epic]: updateEpicLabelsMutation,
+ [TYPE_EPIC]: updateEpicLabelsMutation,
+ [IssuableType.TestCase]: updateTestCaseLabelsMutation,
};
describe('LabelsSelectRoot', () => {
@@ -50,7 +52,7 @@ describe('LabelsSelectRoot', () => {
const createComponent = ({
config = mockConfig,
slots = {},
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
} = {}) => {
@@ -211,9 +213,10 @@ describe('LabelsSelectRoot', () => {
describe.each`
issuableType
- ${IssuableType.Issue}
+ ${TYPE_ISSUE}
${IssuableType.MergeRequest}
- ${IssuableType.Epic}
+ ${TYPE_EPIC}
+ ${IssuableType.TestCase}
`('when updating labels for $issuableType', ({ issuableType }) => {
const label = { id: 'gid://gitlab/ProjectLabel/2' };
@@ -228,6 +231,7 @@ describe('LabelsSelectRoot', () => {
it('updates labels correctly after successful mutation', async () => {
createComponent({ issuableType });
+
await nextTick();
findDropdownContents().vm.$emit('setLabels', [label]);
await waitForPromises();
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 48530a0261f..5d5a7e9a200 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
@@ -174,6 +174,15 @@ export const updateLabelsMutationResponse = {
updateIssuableLabels: {
errors: [],
issuable: {
+ updatedAt: '2023-02-10T22:26:49Z',
+ updatedBy: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'avatar/url',
+ name: 'John Smith',
+ username: 'jsmith',
+ webUrl: 'http://gdk.test:3000/jsmith',
+ __typename: 'UserCore',
+ },
__typename: 'Issue',
id: '1',
labels: {
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
index 843ac1da4bb..b492753867b 100644
--- a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale';
import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
@@ -11,7 +11,7 @@ describe('MilestoneDropdown component', () => {
const propsData = {
attrWorkspacePath: 'full/path',
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
workspaceType: WorkspaceType.project,
};
diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
new file mode 100644
index 00000000000..acd6b23c1f5
--- /dev/null
+++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
@@ -0,0 +1,157 @@
+import { shallowMount } from '@vue/test-utils';
+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 { visitUrl } from '~/lib/utils/url_utility';
+import { createAlert } from '~/flash';
+import ProjectSelect from '~/sidebar/components/move/issuable_move_dropdown.vue';
+import MoveIssueButton from '~/sidebar/components/move/move_issue_button.vue';
+import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+const projectFullPath = 'flight/FlightJS';
+const projectsAutocompleteEndpoint = '/-/autocomplete/projects?project_id=1';
+const issueIid = '15';
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockWebUrl = `${mockDestinationProject.full_path}/issues/${issueIid}`;
+
+const mockMutationErrorMessage = 'Example error message';
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ issue: {
+ id: issueIid,
+ webUrl: mockWebUrl,
+ },
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+describe('MoveIssueButton', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findProjectSelect = () => wrapper.findComponent(ProjectSelect);
+ const emitProjectSelectEvent = () => {
+ findProjectSelect().vm.$emit('move-issuable', mockDestinationProject);
+ };
+ const createComponent = (mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([[moveIssueMutation, mutationResolverMock]]);
+
+ wrapper = shallowMount(MoveIssueButton, {
+ provide: {
+ projectFullPath,
+ projectsAutocompleteEndpoint,
+ issueIid,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ it('renders the project select dropdown', () => {
+ createComponent();
+
+ expect(findProjectSelect().props()).toMatchObject({
+ projectsFetchPath: projectsAutocompleteEndpoint,
+ dropdownButtonTitle: MoveIssueButton.i18n.title,
+ dropdownHeaderTitle: MoveIssueButton.i18n.title,
+ moveInProgress: false,
+ });
+ });
+
+ describe('when the project is selected', () => {
+ it('sets loading state and dropdown button text when issue is moving', async () => {
+ createComponent();
+ expect(findProjectSelect().props()).toMatchObject({
+ dropdownButtonTitle: MoveIssueButton.i18n.title,
+ moveInProgress: false,
+ });
+
+ emitProjectSelectEvent();
+ await nextTick();
+
+ expect(findProjectSelect().props()).toMatchObject({
+ dropdownButtonTitle: MoveIssueButton.i18n.titleInProgress,
+ moveInProgress: true,
+ });
+ });
+
+ it.each`
+ condition | mutation
+ ${'a mutation returns errors'} | ${resolvedMutationWithErrorsMock}
+ ${'a mutation is rejected'} | ${rejectedMutationMock}
+ `('sets loading state to false when $condition', async ({ mutation }) => {
+ createComponent(mutation);
+ emitProjectSelectEvent();
+
+ await nextTick();
+ expect(findProjectSelect().props('moveInProgress')).toBe(true);
+
+ await waitForPromises();
+ expect(findProjectSelect().props('moveInProgress')).toBe(false);
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(resolvedMutationWithErrorsMock);
+ emitProjectSelectEvent();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: MoveIssueButton.i18n.moveErrorMessage,
+ captureError: true,
+ error: expect.any(Object),
+ });
+ });
+
+ it('calls a mutation for the selected issue', async () => {
+ createComponent(resolvedMutationWithoutErrorsMock);
+ emitProjectSelectEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledWith({
+ moveIssueInput: {
+ projectPath: projectFullPath,
+ iid: issueIid,
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+
+ it('redirects to the correct page when the mutation succeeds', async () => {
+ createComponent(resolvedMutationWithoutErrorsMock);
+ emitProjectSelectEvent();
+ await waitForPromises();
+
+ expect(visitUrl).toHaveBeenCalledWith(mockWebUrl);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/move/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index 999340da27c..c65bad642a0 100644
--- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -75,9 +75,15 @@ if (IS_EE) {
getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
}
+const mockIssueResult = {
+ id: mockIssue.iid,
+ webUrl: `${mockDestinationProject.full_path}/issues/${mockIssue.iid}`,
+};
+
const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
data: {
issueMove: {
+ issue: mockIssueResult,
errors: [],
},
},
@@ -86,6 +92,7 @@ const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
data: {
issueMove: {
+ issue: mockIssueResult,
errors: [{ message: mockMutationErrorMessage }],
},
},
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 8f936240b7a..71c6c259c32 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
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';
+import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue';
jest.mock('~/flash');
@@ -27,7 +27,7 @@ describe('SidebarSeverity', () => {
...props,
};
mutate = jest.fn();
- wrapper = shallowMountExtended(SidebarSeverity, {
+ wrapper = mountExtended(SidebarSeverityWidget, {
propsData,
provide: {
canUpdate,
@@ -48,13 +48,11 @@ describe('SidebarSeverity', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
+ wrapper.destroy();
});
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
- const findEditBtn = () => wrapper.findByTestId('editButton');
+ const findEditBtn = () => wrapper.findByTestId('edit-button');
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -127,30 +125,15 @@ describe('SidebarSeverity', () => {
});
describe('Switch between collapsed/expanded view of the sidebar', () => {
- const HIDDDEN_CLASS = 'gl-display-none';
- const SHOWN_CLASS = 'show';
-
describe('collapsed', () => {
it('should have collapsed icon class', () => {
expect(findCollapsedSeverity().classes('sidebar-collapsed-icon')).toBe(true);
});
it('should display only icon with a tooltip', () => {
- expect(findSeverityToken().at(0).attributes('icononly')).toBe('true');
- expect(findSeverityToken().at(0).attributes('iconsize')).toBe('14');
- expect(findTooltip().text().replace(/\s+/g, ' ')).toContain(
- `Severity: ${INCIDENT_SEVERITY[severity].label}`,
- );
- });
-
- it('should expand the dropdown on collapsed icon click', async () => {
- wrapper.vm.isDropdownShowing = false;
- await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
-
- findCollapsedSeverity().trigger('click');
- await nextTick();
- expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+ expect(findSeverityToken().exists()).toBe(true);
+ expect(findTooltip().text()).toContain(INCIDENT_SEVERITY[severity].label);
+ expect(findEditBtn().exists()).toBe(false);
});
});
@@ -158,17 +141,16 @@ describe('SidebarSeverity', () => {
it('toggles dropdown with edit button', async () => {
canUpdate = true;
createComponent();
- wrapper.vm.isDropdownShowing = false;
await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(false);
findEditBtn().vm.$emit('click');
await nextTick();
- expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(true);
findEditBtn().vm.$emit('click');
await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
index 83bc8cf7002..9f3d689edee 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -11,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
@@ -66,7 +66,7 @@ describe('SidebarDropdown component', () => {
propsData: {
attrWorkspacePath: mockIssue.projectPath,
currentAttribute: {},
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
attachTo: document.body,
@@ -83,7 +83,7 @@ describe('SidebarDropdown component', () => {
propsData: {
attrWorkspacePath: mockIssue.projectPath,
currentAttribute: {},
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
...props,
},
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index cf5e220a705..060a2873e04 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -9,7 +9,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
@@ -105,7 +105,7 @@ describe('SidebarDropdownWidget', () => {
workspacePath: mockIssue.projectPath,
attrWorkspacePath: mockIssue.projectPath,
iid: mockIssue.iid,
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
attachTo: document.body,
@@ -126,7 +126,7 @@ describe('SidebarDropdownWidget', () => {
workspacePath: '',
attrWorkspacePath: '',
iid: '',
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
mocks: {
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
index cb3bb7a4538..715f66d305a 100644
--- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -7,7 +7,7 @@ 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';
+import { TYPENAME_ISSUE, TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
const mockMutationErrorMessage = 'Example error message';
@@ -157,8 +157,8 @@ describe('Create Timelog Form', () => {
it.each`
issuableType | typeConstant
- ${'issue'} | ${TYPE_ISSUE}
- ${'merge_request'} | ${TYPE_MERGE_REQUEST}
+ ${'issue'} | ${TYPENAME_ISSUE}
+ ${'merge_request'} | ${TYPENAME_MERGE_REQUEST}
`(
'calls the mutation with all the fields when the the form is submitted and issuable type is $issuableType',
async ({ issuableType, typeConstant }) => {
diff --git a/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
deleted file mode 100644
index 6e365df329b..00000000000
--- a/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
-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';
-
-jest.mock('~/flash');
-
-describe('SidebarMoveIssue', () => {
- let mock;
- const test = {};
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
- mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
- test.mediator = new SidebarMediator(Mock.mediator);
- test.$content = $(`
- <div class="dropdown">
- <div class="js-toggle"></div>
- <div class="dropdown-menu">
- <div class="dropdown-content"></div>
- </div>
- <div class="js-confirm-button"></div>
- </div>
- `);
- test.$toggleButton = test.$content.find('.js-toggle');
- test.$confirmButton = test.$content.find('.js-confirm-button');
-
- test.sidebarMoveIssue = new SidebarMoveIssue(
- test.mediator,
- test.$toggleButton,
- test.$confirmButton,
- );
- test.sidebarMoveIssue.init();
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
-
- test.sidebarMoveIssue.destroy();
- mock.restore();
- });
-
- describe('init', () => {
- it('should initialize the dropdown and listeners', () => {
- jest.spyOn(test.sidebarMoveIssue, 'initDropdown').mockImplementation(() => {});
- jest.spyOn(test.sidebarMoveIssue, 'addEventListeners').mockImplementation(() => {});
-
- test.sidebarMoveIssue.init();
-
- expect(test.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
- expect(test.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('destroy', () => {
- it('should remove the listeners', () => {
- jest.spyOn(test.sidebarMoveIssue, 'removeEventListeners').mockImplementation(() => {});
-
- test.sidebarMoveIssue.destroy();
-
- expect(test.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('initDropdown', () => {
- it('should initialize the deprecatedJQueryDropdown', () => {
- test.sidebarMoveIssue.initDropdown();
-
- expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeInstanceOf(
- GitLabDropdown,
- );
- });
-
- it('escapes html from project name', async () => {
- test.$toggleButton.dropdown('toggle');
-
- await waitForPromises();
-
- expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
- '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
- );
- });
- });
-
- describe('onConfirmClicked', () => {
- it('should move the issue with valid project ID', () => {
- jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.resolve());
- test.mediator.setMoveToProjectId(7);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBe(true);
- expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
- });
-
- it('should remove loading state from confirm button on failure', async () => {
- jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
- test.mediator.setMoveToProjectId(7);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).toHaveBeenCalled();
-
- // Wait for the move issue request to fail
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBe(false);
- expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
- });
-
- it('should not move the issue with id=0', () => {
- jest.spyOn(test.mediator, 'moveIssue').mockImplementation(() => {});
- test.mediator.setMoveToProjectId(0);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).not.toHaveBeenCalled();
- });
- });
-
- it('should set moveToProjectId on dropdown item "No project" click', async () => {
- jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
-
- // Open the dropdown
- test.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- await waitForPromises();
-
- test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
-
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(test.$confirmButton.prop('disabled')).toBe(true);
- });
-
- it('should set moveToProjectId on dropdown item click', async () => {
- jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
-
- // Open the dropdown
- test.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- await waitForPromises();
-
- test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
-
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
- expect(test.$confirmButton.attr('disabled')).toBe(undefined);
- });
-});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index cdb9ced70b8..77b1ccb4f9a 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
@@ -36,10 +37,10 @@ describe('Sidebar mediator', () => {
});
it('saves assignees', () => {
- mock.onPut(mediatorMockData.endpoint).reply(200, {});
+ mock.onPut(mediatorMockData.endpoint).reply(HTTP_STATUS_OK, {});
return mediator.saveAssignees('issue[assignee_ids]').then((resp) => {
- expect(resp.status).toEqual(200);
+ expect(resp.status).toEqual(HTTP_STATUS_OK);
});
});
@@ -91,7 +92,7 @@ describe('Sidebar mediator', () => {
it('fetches the data', async () => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
+ mock.onGet(mediatorMockData.endpoint).reply(HTTP_STATUS_OK, mockData);
const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
await mediator.fetch();
@@ -120,7 +121,7 @@ describe('Sidebar mediator', () => {
it('fetches autocomplete projects', () => {
const searchTerm = 'foo';
- mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(HTTP_STATUS_OK, {});
const getterSpy = jest
.spyOn(mediator.service, 'getProjectsAutocomplete')
.mockReturnValue(Promise.resolve({ data: {} }));
@@ -137,7 +138,7 @@ describe('Sidebar mediator', () => {
it('moves issue', () => {
const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
const moveToProjectId = 7;
- mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(HTTP_STATUS_OK, mockData);
mediator.store.setMoveToProjectId(moveToProjectId);
const moveIssueSpy = jest
.spyOn(mediator.service, 'moveIssue')
diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js
index 6f42ec47458..ff2a4e31e0b 100644
--- a/spec/frontend/single_file_diff_spec.js
+++ b/spec/frontend/single_file_diff_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SingleFileDiff from '~/single_file_diff';
describe('SingleFileDiff', () => {
@@ -10,7 +11,9 @@ describe('SingleFileDiff', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(blobDiffPath).replyOnce(200, { html: `<div class="diff-content">MOCKED</div>` });
+ mock
+ .onGet(blobDiffPath)
+ .replyOnce(HTTP_STATUS_OK, { html: `<div class="diff-content">MOCKED</div>` });
});
afterEach(() => {
@@ -54,7 +57,7 @@ describe('SingleFileDiff', () => {
expect(diff.isOpen).toBe(false);
expect(diff.content).not.toBeNull();
- mock.onGet(blobDiffPath).replyOnce(400, '');
+ mock.onGet(blobDiffPath).replyOnce(HTTP_STATUS_BAD_REQUEST, '');
// Opening again
await diff.toggleDiff($(document.querySelector('.js-file-title')));
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 33b8e2be969..82c4a37ccc9 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
@@ -57,7 +58,7 @@ describe('Snippet Blob Edit component', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_CONTENT);
+ axiosMock.onGet(TEST_FULL_PATH).reply(HTTP_STATUS_OK, TEST_CONTENT);
});
afterEach(() => {
@@ -103,7 +104,7 @@ describe('Snippet Blob Edit component', () => {
describe('with unloaded blob and JSON content', () => {
beforeEach(() => {
- axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_JSON_CONTENT);
+ axiosMock.onGet(TEST_FULL_PATH).reply(HTTP_STATUS_OK, TEST_JSON_CONTENT);
createComponent();
});
@@ -118,7 +119,7 @@ describe('Snippet Blob Edit component', () => {
describe('with error', () => {
beforeEach(() => {
axiosMock.reset();
- axiosMock.onGet(TEST_FULL_PATH).replyOnce(500);
+ axiosMock.onGet(TEST_FULL_PATH).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
});
diff --git a/spec/frontend/super_sidebar/components/counter_spec.js b/spec/frontend/super_sidebar/components/counter_spec.js
index 1150b0a3aa8..8f514540413 100644
--- a/spec/frontend/super_sidebar/components/counter_spec.js
+++ b/spec/frontend/super_sidebar/components/counter_spec.js
@@ -13,10 +13,6 @@ describe('Counter component', () => {
label: __('Issues'),
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.find('button');
const findIcon = () => wrapper.getComponent(GlIcon);
const findLink = () => wrapper.find('a');
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
new file mode 100644
index 00000000000..b24c6b8de7f
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -0,0 +1,39 @@
+import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
+import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import { createNewMenuGroups } from '../mock_data';
+
+describe('CreateMenu component', () => {
+ let wrapper;
+
+ const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(CreateMenu, {
+ propsData: {
+ groups: createNewMenuGroups,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("sets the toggle's label", () => {
+ expect(findGlDisclosureDropdown().props('toggleText')).toBe(__('Create new...'));
+ });
+
+ it('passes the groups to the disclosure dropdown', () => {
+ expect(findGlDisclosureDropdown().props('items')).toBe(createNewMenuGroups);
+ });
+
+ it("sets the toggle ID and tooltip's target", () => {
+ expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
+ expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
new file mode 100644
index 00000000000..bc847a3e159
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -0,0 +1,152 @@
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { within } from '@testing-library/dom';
+import toggleWhatsNewDrawer from '~/whats_new';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { STORAGE_KEY } from '~/whats_new/utils/notification';
+import { sidebarData } from '../mock_data';
+
+jest.mock('~/whats_new');
+
+describe('HelpCenter component', () => {
+ let wrapper;
+
+ const GlEmoji = { template: '<img/>' };
+
+ const findDropdownGroup = (i = 0) => {
+ return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
+ };
+ const withinComponent = () => within(wrapper.element);
+ const findButton = (name) => withinComponent().getByRole('button', { name });
+
+ // eslint-disable-next-line no-shadow
+ const createWrapper = (sidebarData) => {
+ wrapper = mountExtended(HelpCenter, {
+ propsData: { sidebarData },
+ stubs: { GlEmoji },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper(sidebarData);
+ });
+
+ it('renders menu items', () => {
+ expect(findDropdownGroup(0).props('group').items).toEqual([
+ { text: HelpCenter.i18n.help, href: helpPagePath() },
+ { text: HelpCenter.i18n.support, href: sidebarData.support_path },
+ { text: HelpCenter.i18n.docs, href: 'https://docs.gitlab.com' },
+ { text: HelpCenter.i18n.plans, href: `${PROMO_URL}/pricing` },
+ { text: HelpCenter.i18n.forum, href: 'https://forum.gitlab.com/' },
+ {
+ text: HelpCenter.i18n.contribute,
+ href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ },
+ { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
+ ]);
+
+ expect(findDropdownGroup(1).props('group').items).toEqual([
+ expect.objectContaining({ text: HelpCenter.i18n.shortcuts }),
+ expect.objectContaining({ text: HelpCenter.i18n.whatsnew }),
+ ]);
+ });
+
+ describe('with Gitlab version check feature enabled', () => {
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_version_check: true });
+ });
+
+ it('shows version information as first item', () => {
+ expect(findDropdownGroup(0).props('group').items).toEqual([
+ { text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' },
+ ]);
+ });
+ });
+
+ describe('showKeyboardShortcuts', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ window.toggleShortcutsHelp = jest.fn();
+ findButton('Keyboard shortcuts ?').click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the keyboard shortcuts modal', () => {
+ expect(window.toggleShortcutsHelp).toHaveBeenCalled();
+ });
+ });
+
+ describe('showWhatsNew', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ findButton("What's new 5").click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the "What\'s new" slideout', () => {
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(expect.any(Object));
+ });
+
+ it('shows the existing "What\'s new" slideout instance on subsequent clicks', () => {
+ findButton("What's new").click();
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
+ expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
+ });
+ });
+
+ describe('shouldShowWhatsNewNotification', () => {
+ describe('when setting is disabled', () => {
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, display_whats_new: false });
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+
+ describe('when setting is enabled', () => {
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, display_whats_new: true });
+ });
+
+ it('is true', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(true);
+ });
+
+ describe('when "What\'s new" drawer got opened', () => {
+ beforeEach(() => {
+ findButton("What's new 5").click();
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+
+ describe('with matching version digest in local storage', () => {
+ beforeEach(() => {
+ window.localStorage.setItem(STORAGE_KEY, 1);
+ createWrapper({ ...sidebarData, display_whats_new: true });
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
new file mode 100644
index 00000000000..fe87c4be9c3
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -0,0 +1,46 @@
+import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
+import { mergeRequestMenuGroup } from '../mock_data';
+
+describe('MergeRequestMenu component', () => {
+ let wrapper;
+
+ const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
+ const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findLink = () => wrapper.findByRole('link');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(MergeRequestMenu, {
+ propsData: {
+ items: mergeRequestMenuGroup,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes the items to the disclosure dropdown', () => {
+ expect(findGlDisclosureDropdown().props('items')).toBe(mergeRequestMenuGroup);
+ });
+
+ it('renders item text and count in link', () => {
+ const { text, href, count } = mergeRequestMenuGroup[0].items[0];
+ expect(findLink().text()).toContain(text);
+ expect(findLink().text()).toContain(String(count));
+ expect(findLink().attributes('href')).toBe(href);
+ });
+
+ it('renders item count string in badge', () => {
+ const { count } = mergeRequestMenuGroup[0].items[0];
+ expect(findGlBadge(0).text()).toBe(String(count));
+ });
+
+ it('renders 0 string when count is empty', () => {
+ expect(findGlBadge(1).text()).toBe(String(0));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index d7d2f67dc8a..45fc30c08f0 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,5 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -7,10 +8,7 @@ describe('SuperSidebar component', () => {
let wrapper;
const findUserBar = () => wrapper.findComponent(UserBar);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
@@ -29,5 +27,9 @@ describe('SuperSidebar component', () => {
it('renders UserBar with sidebarData', () => {
expect(findUserBar().props('sidebarData')).toBe(sidebarData);
});
+
+ it('renders HelpCenter with sidebarData', () => {
+ expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index 6d0186a2749..eceb792c3db 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,5 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
+import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -7,11 +9,9 @@ import { sidebarData } from '../mock_data';
describe('UserBar component', () => {
let wrapper;
+ const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(UserBar, {
@@ -31,12 +31,25 @@ describe('UserBar component', () => {
createWrapper();
});
+ it('passes the "Create new..." menu groups to the create-menu component', () => {
+ expect(findCreateMenu().props('groups')).toBe(sidebarData.create_new_menu_groups);
+ });
+
+ it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
+ expect(findMergeRequestMenu().props('items')).toBe(sidebarData.merge_request_menu);
+ });
+
it('renders issues counter', () => {
expect(findCounter(0).props('count')).toBe(sidebarData.assigned_open_issues_count);
expect(findCounter(0).props('href')).toBe(sidebarData.issues_dashboard_path);
expect(findCounter(0).props('label')).toBe(__('Issues'));
});
+ it('renders merge requests counter', () => {
+ expect(findCounter(1).props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(findCounter(1).props('label')).toBe(__('Merge requests'));
+ });
+
it('renders todos counter', () => {
expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
expect(findCounter(2).props('href')).toBe('/dashboard/todos');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 7db0d0ea5cc..91a2dc93a47 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -1,9 +1,77 @@
+export const createNewMenuGroups = [
+ {
+ name: 'This group',
+ items: [
+ {
+ text: 'New project/repository',
+ href: '/projects/new?namespace_id=22',
+ },
+ {
+ text: 'New subgroup',
+ href: '/groups/new?parent_id=22#create-group-pane',
+ },
+ {
+ text: 'New epic',
+ href: '/groups/gitlab-org/-/epics/new',
+ },
+ {
+ text: 'Invite members',
+ href: '/groups/gitlab-org/-/group_members',
+ },
+ ],
+ },
+ {
+ name: 'GitLab',
+ items: [
+ {
+ text: 'New project/repository',
+ href: '/projects/new',
+ },
+ {
+ text: 'New group',
+ href: '/groups/new',
+ },
+ {
+ text: 'New snippet',
+ href: '/-/snippets/new',
+ },
+ ],
+ },
+];
+
+export const mergeRequestMenuGroup = [
+ {
+ name: 'Merge requests',
+ items: [
+ {
+ text: 'Assigned',
+ href: '/dashboard/merge_requests?assignee_username=root',
+ count: 4,
+ },
+ {
+ text: 'Review requests',
+ href: '/dashboard/merge_requests?reviewer_username=root',
+ count: 0,
+ },
+ ],
+ },
+];
+
export const sidebarData = {
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
assigned_open_issues_count: 1,
- assigned_open_merge_requests_count: 2,
todos_pending_count: 3,
issues_dashboard_path: 'path/to/issues',
+ total_merge_requests_count: 4,
+ create_new_menu_groups: createNewMenuGroups,
+ merge_request_menu: mergeRequestMenuGroup,
+ support_path: '/support',
+ display_whats_new: true,
+ whats_new_most_recent_release_items_count: 5,
+ whats_new_version_digest: 1,
+ show_version_check: false,
+ gitlab_version: { major: 16, minor: 0 },
+ gitlab_version_check: { severity: 'success' },
};
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index ce1c126f868..99f61a31dbd 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -3,7 +3,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
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';
@@ -129,7 +128,6 @@ describe('TermsApp', () => {
beforeEach(() => {
flashEl = document.createElement('div');
- flashEl.classList.add(`flash-${FLASH_TYPES.ALERT}`);
document.body.appendChild(flashEl);
});
@@ -137,7 +135,7 @@ describe('TermsApp', () => {
document.body.innerHTML = '';
});
- it('recalculates height of scrollable viewport', () => {
+ it('recalculates height of scrollable viewport', async () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
@@ -148,7 +146,8 @@ describe('TermsApp', () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 700);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
- flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
+ flashEl.remove();
+ await nextTick();
expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 100px);');
});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
new file mode 100644
index 00000000000..fcd1a33fa68
--- /dev/null
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -0,0 +1,311 @@
+import { GlAlert, GlFormInput, GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
+import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
+import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
+import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
+import inboundGetCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
+import inboundGetProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql';
+import {
+ inboundJobTokenScopeEnabledResponse,
+ inboundJobTokenScopeDisabledResponse,
+ inboundProjectsWithScopeResponse,
+ inboundAddProjectSuccessResponse,
+ inboundRemoveProjectSuccess,
+ inboundUpdateScopeSuccessResponse,
+} from './mock_data';
+
+const projectPath = 'root/my-repo';
+const message = 'An error occurred';
+const error = new Error(message);
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('TokenAccess component', () => {
+ let wrapper;
+
+ const inboundJobTokenScopeEnabledResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundJobTokenScopeEnabledResponse);
+ const inboundJobTokenScopeDisabledResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundJobTokenScopeDisabledResponse);
+ const inboundProjectsWithScopeResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundProjectsWithScopeResponse);
+ const inboundAddProjectSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundAddProjectSuccessResponse);
+ const inboundRemoveProjectSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(inboundRemoveProjectSuccess);
+ const inboundUpdateScopeSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundUpdateScopeSuccessResponse);
+ const failureHandler = jest.fn().mockRejectedValue(error);
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
+ const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' });
+ const findProjectInput = () => wrapper.findComponent(GlFormInput);
+ const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
+ const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
+
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(InboundTokenAccess, {
+ provide: {
+ fullPath: projectPath,
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ data() {
+ return {
+ targetProjectPath: 'root/test',
+ };
+ },
+ });
+ };
+
+ describe('loading state', () => {
+ it('shows loading state while waiting on query to resolve', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('fetching projects and scope', () => {
+ it('fetches projects and scope correctly', () => {
+ const expectedVariables = {
+ fullPath: 'root/my-repo',
+ };
+
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables);
+ expect(inboundProjectsWithScopeResponseHandler).toHaveBeenCalledWith(expectedVariables);
+ });
+
+ it('handles fetch projects error correctly', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the projects',
+ });
+ });
+
+ it('handles fetch scope error correctly', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, failureHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ 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([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(false);
+ });
+
+ it('the toggle is off and the alert is visible', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findTokenDisabledAlert().exists()).toBe(true);
+ });
+
+ describe('update ci job token scope', () => {
+ it('calls inboundUpdateCIJobTokenScopeMutation mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+
+ findToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(inboundUpdateScopeSuccessResponseHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ inboundJobTokenScopeEnabled: false,
+ },
+ });
+ });
+
+ it('handles update scope error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
+ [inboundUpdateCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+
+ findToggle().vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
+ });
+
+ describe('add project', () => {
+ it('calls add project mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundAddProjectCIJobTokenScopeMutation, inboundAddProjectSuccessResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({
+ projectPath,
+ targetProjectPath: 'root/test',
+ });
+ });
+
+ it('add project handles error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundAddProjectCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+
+ it('clicking cancel clears target path', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findProjectInput().element.value).toBe('root/test');
+
+ await findCancelBtn().trigger('click');
+
+ expect(findProjectInput().element.value).toBe('');
+ });
+ });
+
+ describe('remove project', () => {
+ it('calls remove project mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundRemoveProjectCIJobTokenScopeMutation, inboundRemoveProjectSuccessHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ expect(inboundRemoveProjectSuccessHandler).toHaveBeenCalledWith({
+ projectPath,
+ targetProjectPath: 'root/ci-project',
+ });
+ });
+
+ it('remove project handles error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundRemoveProjectCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
+});
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 0c8ba266201..ab04735b985 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -105,3 +105,125 @@ export const mockProjects = [
__typename: 'Project',
},
];
+
+export const mockFields = [
+ {
+ key: 'project',
+ label: 'Project with access',
+ },
+ {
+ key: 'namespace',
+ label: 'Namespace',
+ },
+ {
+ key: 'actions',
+ label: '',
+ },
+];
+
+export const optInJwtQueryResponse = (optInJwt) => ({
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ optInJwt,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
+export const optInJwtMutationResponse = (optInJwt) => ({
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ optInJwt,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+});
+
+export const inboundJobTokenScopeEnabledResponse = {
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: true,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const inboundJobTokenScopeDisabledResponse = {
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const inboundProjectsWithScopeResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: '1',
+ ciJobTokenScope: {
+ __typename: 'CiJobTokenScopeType',
+ inboundAllowlist: {
+ __typename: 'ProjectConnection',
+ nodes: [
+ {
+ __typename: 'Project',
+ fullPath: 'root/ci-project',
+ id: 'gid://gitlab/Project/23',
+ name: 'ci-project',
+ namespace: { id: 'gid://gitlab/Namespaces::UserNamespace/1', fullPath: 'root' },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const inboundAddProjectSuccessResponse = {
+ data: {
+ ciJobTokenScopeAddProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeAddProjectPayload',
+ },
+ },
+};
+
+export const inboundRemoveProjectSuccess = {
+ data: {
+ ciJobTokenScopeRemoveProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeRemoveProjectPayload',
+ },
+ },
+};
+
+export const inboundUpdateScopeSuccessResponse = {
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+};
diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js
new file mode 100644
index 00000000000..3a68f247aa6
--- /dev/null
+++ b/spec/frontend/token_access/opt_in_jwt_spec.js
@@ -0,0 +1,144 @@
+import { GlLink, GlLoadingIcon, GlToggle, GlSprintf } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants';
+import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
+import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql';
+import updateOptInJwtMutation from '~/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql';
+import { optInJwtMutationResponse, optInJwtQueryResponse } from './mock_data';
+
+const errorMessage = 'An error occurred';
+const error = new Error(errorMessage);
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('OptInJwt component', () => {
+ let wrapper;
+
+ const failureHandler = jest.fn().mockRejectedValue(error);
+ const enabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(true));
+ const disabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(false));
+ const updateOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtMutationResponse(true));
+
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findOptInJwtExpandedSection = () => wrapper.findByTestId('opt-in-jwt-expanded-section');
+
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended, options = {}) => {
+ wrapper = mountFn(OptInJwt, {
+ provide: {
+ fullPath: 'root/my-repo',
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ data() {
+ return {
+ targetProjectPath: 'root/test',
+ };
+ },
+ ...options,
+ });
+ };
+
+ const createShallowComponent = (requestHandlers, options = {}) =>
+ createComponent(requestHandlers, shallowMountExtended, options);
+ const createFullComponent = (requestHandlers, options = {}) =>
+ createComponent(requestHandlers, mountExtended, options);
+
+ describe('loading state', () => {
+ it('shows loading state and hides toggle while waiting on query to resolve', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findToggle().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findToggle().exists()).toBe(true);
+ });
+ });
+
+ describe('template', () => {
+ it('renders help link', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]], {
+ stubs: {
+ GlToggle,
+ GlSprintf,
+ GlLink,
+ },
+ });
+ await waitForPromises();
+
+ expect(findHelpLink().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(OPT_IN_JWT_HELP_LINK);
+ });
+ });
+
+ describe('toggle JWT token access', () => {
+ it('code instruction is visible when toggle is enabled', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+ expect(findOptInJwtExpandedSection().exists()).toBe(true);
+ });
+
+ it('code instruction is hidden when toggle is disabled', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, disabledOptInJwtHandler]]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findOptInJwtExpandedSection().exists()).toBe(false);
+ });
+
+ describe('update JWT token access', () => {
+ it('calls updateOptInJwtMutation with correct arguments', async () => {
+ createFullComponent([
+ [getOptInJwtSettingQuery, disabledOptInJwtHandler],
+ [updateOptInJwtMutation, updateOptInJwtHandler],
+ ]);
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', true);
+
+ expect(updateOptInJwtHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ optInJwt: true,
+ },
+ });
+ });
+
+ it('handles update error', async () => {
+ createFullComponent([
+ [getOptInJwtSettingQuery, enabledOptInJwtHandler],
+ [updateOptInJwtMutation, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while update the setting. Please try again.',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 6fe94e28548..893a021197f 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import TokenAccess from '~/token_access/components/token_access.vue';
+import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
@@ -50,7 +50,7 @@ describe('TokenAccess component', () => {
};
const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
- wrapper = mountFn(TokenAccess, {
+ wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
},
@@ -63,10 +63,6 @@ describe('TokenAccess component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js
new file mode 100644
index 00000000000..7f269ee5fda
--- /dev/null
+++ b/spec/frontend/token_access/token_access_app_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
+import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
+import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
+import TokenAccessApp from '~/token_access/components/token_access_app.vue';
+
+describe('TokenAccessApp component', () => {
+ let wrapper;
+
+ const findOutboundTokenAccess = () => wrapper.findComponent(OutboundTokenAccess);
+ const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess);
+ const findOptInJwt = () => wrapper.findComponent(OptInJwt);
+
+ const createComponent = (flagState = false) => {
+ wrapper = shallowMount(TokenAccessApp, {
+ provide: {
+ glFeatures: { ciInboundJobTokenScope: flagState },
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the opt in jwt component', () => {
+ expect(findOptInJwt().exists()).toBe(true);
+ });
+
+ it('renders the outbound token access component', () => {
+ expect(findOutboundTokenAccess().exists()).toBe(true);
+ });
+
+ it('does not render the inbound token access component', () => {
+ expect(findInboundTokenAccess().exists()).toBe(false);
+ });
+ });
+
+ describe('with feature flag enabled', () => {
+ it('renders the inbound token access component', () => {
+ createComponent(true);
+
+ expect(findInboundTokenAccess().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 0fa1a2453f7..b51d8b3ccea 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -1,7 +1,7 @@
import { GlTable, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
-import { mockProjects } from './mock_data';
+import { mockProjects, mockFields } from './mock_data';
describe('Token projects table', () => {
let wrapper;
@@ -12,6 +12,7 @@ describe('Token projects table', () => {
fullPath: 'root/ci-project',
},
propsData: {
+ tableFields: mockFields,
projects: mockProjects,
},
});
@@ -28,10 +29,6 @@ describe('Token projects table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/tracking/get_standard_context_spec.js b/spec/frontend/tracking/get_standard_context_spec.js
index ada914b586c..ae452aeaeb3 100644
--- a/spec/frontend/tracking/get_standard_context_spec.js
+++ b/spec/frontend/tracking/get_standard_context_spec.js
@@ -52,7 +52,7 @@ describe('~/tracking/get_standard_context', () => {
it('accepts optional `extra` property', () => {
const extra = { foo: 'bar' };
- expect(getStandardContext({ extra }).data.extra).toBe(extra);
+ expect(getStandardContext({ extra }).data.extra).toStrictEqual(extra);
});
describe('with Google Analytics cookie present', () => {
diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
new file mode 100644
index 00000000000..cb70ea4e72d
--- /dev/null
+++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
@@ -0,0 +1,39 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UsageQuotasApp from '~/usage_quotas/components/usage_quotas_app.vue';
+import { USAGE_QUOTAS_TITLE } from '~/usage_quotas/constants';
+import { defaultProvide } from '../mock_data';
+
+describe('UsageQuotasApp', () => {
+ let wrapper;
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = shallowMountExtended(UsageQuotasApp, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle');
+
+ it('renders the view title', () => {
+ expect(wrapper.text()).toContain(USAGE_QUOTAS_TITLE);
+ });
+
+ it('renders the view subtitle', () => {
+ expect(findSubTitle().text()).toContain(defaultProvide.namespaceName);
+ });
+});
diff --git a/spec/frontend/usage_quotas/mock_data.js b/spec/frontend/usage_quotas/mock_data.js
new file mode 100644
index 00000000000..a9d2a7ad1db
--- /dev/null
+++ b/spec/frontend/usage_quotas/mock_data.js
@@ -0,0 +1,3 @@
+export const defaultProvide = {
+ namespaceName: 'Group 1',
+};
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
index 7ad28566f49..1ef856c9849 100644
--- a/spec/frontend/users/profile/components/report_abuse_button_spec.js
+++ b/spec/frontend/users/profile/components/report_abuse_button_spec.js
@@ -9,7 +9,7 @@ describe('ReportAbuseButton', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = '1';
+ const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 9231e38ea90..6fb3436100f 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -4,6 +4,7 @@ import usersFixture from 'test_fixtures/autocomplete/users.json';
import { getFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UsersSelect from '~/users_select';
// fixtures -------------------------------------------------------------------
@@ -31,7 +32,7 @@ export const createTestContext = ({ fixturePath }) => {
document.body.appendChild(rootEl);
mock = new MockAdapter(axios);
- mock.onGet('/-/autocomplete/users.json').reply(200, cloneDeep(getUsersFixture()));
+ mock.onGet('/-/autocomplete/users.json').reply(HTTP_STATUS_OK, cloneDeep(getUsersFixture()));
};
const teardown = () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 1f3b6dce620..bf208f16d18 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -13,7 +13,12 @@ import {
} from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub';
-jest.mock('~/flash');
+const mockAlertDismiss = jest.fn();
+jest.mock('~/flash', () => ({
+ createAlert: jest.fn().mockImplementation(() => ({
+ dismiss: mockAlertDismiss,
+ })),
+}));
const RULE_NAME = 'first_rule';
const TEST_HELP_PATH = 'help/path';
@@ -87,6 +92,8 @@ describe('MRWidget approvals', () => {
approvalRules: [],
isOpen: true,
state: 'open',
+ targetProjectFullPath: 'gitlab-org/gitlab',
+ iid: '1',
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -98,21 +105,18 @@ describe('MRWidget approvals', () => {
});
describe('when created', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('shows loading message', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ fetchingApprovals: true });
+ it('shows loading message', async () => {
+ service = {
+ fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})),
+ };
- return nextTick().then(() => {
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
+ createComponent();
+ await nextTick();
+ expect(wrapper.text()).toContain(FETCH_LOADING);
});
it('fetches approvals', () => {
+ createComponent();
expect(service.fetchApprovals).toHaveBeenCalled();
});
});
@@ -267,9 +271,16 @@ describe('MRWidget approvals', () => {
return nextTick();
});
- it('flashes error message', () => {
+ it('shows an alert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({ message: APPROVE_ERROR });
});
+
+ it('clears the previous alert', () => {
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(0);
+
+ findAction().vm.$emit('click');
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(1);
+ });
});
});
});
@@ -377,15 +388,14 @@ describe('MRWidget approvals', () => {
});
it('is rendered with props', () => {
- const expected = testApprovals();
const summary = findSummary();
expect(findOptionalSummary().exists()).toBe(false);
expect(summary.exists()).toBe(true);
expect(summary.props()).toMatchObject({
- approvalsLeft: expected.approvals_left,
- rulesLeft: expected.approval_rules_left,
- approvers: testApprovedBy(),
+ projectPath: 'gitlab-org/gitlab',
+ iid: '1',
+ updatedCount: 0,
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index f4234083346..e75ce7c60c9 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
@@ -1,5 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
-import { toNounSeriesText } from '~/lib/utils/grammar';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json';
+import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import {
APPROVED_BY_OTHERS,
@@ -7,25 +14,21 @@ import {
APPROVED_BY_YOU_AND_OTHERS,
} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql';
-const exampleUserId = 1;
-const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map((id) => ({ id }));
-const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit'];
-const TEST_APPROVALS_LEFT = 3;
+Vue.use(VueApollo);
describe('MRWidget approvals summary', () => {
const originalUserId = gon.current_user_id;
let wrapper;
- const createComponent = (props = {}) => {
- wrapper = shallowMount(ApprovalsSummary, {
+ const createComponent = (response = approvedByCurrentUser) => {
+ wrapper = mount(ApprovalsSummary, {
propsData: {
- approved: false,
- approvers: testApprovers(),
- approvalsLeft: TEST_APPROVALS_LEFT,
- rulesLeft: testRulesLeft(),
- ...props,
+ projectPath: 'gitlab-org/gitlab',
+ iid: '1',
},
+ apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]),
});
};
@@ -38,10 +41,10 @@ describe('MRWidget approvals summary', () => {
});
describe('when approved', () => {
- beforeEach(() => {
- createComponent({
- approved: true,
- });
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
});
it('shows approved message', () => {
@@ -54,18 +57,19 @@ describe('MRWidget approvals summary', () => {
expect(avatars.exists()).toBe(true);
expect(avatars.props()).toEqual(
expect.objectContaining({
- items: testApprovers(),
+ items: approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes,
}),
);
});
describe('by the current user', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId }],
- approved: true,
- });
+ beforeEach(async () => {
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
+ createComponent();
+
+ await waitForPromises();
});
it('shows "Approved by you" message', () => {
@@ -74,12 +78,13 @@ describe('MRWidget approvals summary', () => {
});
describe('by the current user and others', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId }, { id: exampleUserId + 1 }],
- approved: true,
- });
+ beforeEach(async () => {
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByMultipleUsers.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
+ createComponent(approvedByMultipleUsers);
+
+ await waitForPromises();
});
it('shows "Approved by you and others" message', () => {
@@ -88,12 +93,10 @@ describe('MRWidget approvals summary', () => {
});
describe('by other users than the current user', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId + 1 }],
- approved: true,
- });
+ beforeEach(async () => {
+ createComponent(approvedByMultipleUsers);
+
+ await waitForPromises();
});
it('shows "Approved by others" message', () => {
@@ -102,37 +105,11 @@ describe('MRWidget approvals summary', () => {
});
});
- describe('when not approved', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('render message', () => {
- const names = toNounSeriesText(testRulesLeft());
-
- expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} approvals from ${names}.`);
- });
- });
-
- describe('when no rulesLeft', () => {
- beforeEach(() => {
- createComponent({
- rulesLeft: [],
- });
- });
-
- it('renders message', () => {
- expect(wrapper.text()).toContain(
- `Requires ${TEST_APPROVALS_LEFT} approvals from eligible users`,
- );
- });
- });
-
describe('when no approvers', () => {
- beforeEach(() => {
- createComponent({
- approvers: [],
- });
+ beforeEach(async () => {
+ createComponent(noApprovalsResponse);
+
+ await waitForPromises();
});
it('does not render avatar list', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index 73fa4b7b08f..52e2393bf05 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
@@ -5,6 +5,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST as FAKE_ENDPOINT } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { getStoreConfig } from '~/vue_merge_request_widget/stores/artifacts_list';
import { artifacts } from '../mock_data';
@@ -78,10 +79,10 @@ describe('Merge Requests Artifacts list app', () => {
describe('with results', () => {
beforeEach(() => {
createComponent();
- mock.onGet(FAKE_ENDPOINT).reply(200, artifacts, {});
+ mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_OK, artifacts, {});
store.dispatch('receiveArtifactsSuccess', {
data: artifacts,
- status: 200,
+ status: HTTP_STATUS_OK,
});
return nextTick();
});
@@ -109,7 +110,7 @@ describe('Merge Requests Artifacts list app', () => {
describe('with error', () => {
beforeEach(() => {
createComponent();
- mock.onGet(FAKE_ENDPOINT).reply(500, {}, {});
+ mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, {});
store.dispatch('receiveArtifactsError');
return nextTick();
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
index 193a16bae8d..4775a0673b5 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
@@ -66,7 +67,7 @@ describe('MemoryUsage', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(`${url}.json`).reply(200);
+ mock.onGet(`${url}.json`).reply(HTTP_STATUS_OK);
vm = createComponent();
el = vm.$el;
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index c3f6331e560..13beb43e10b 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import DeploymentList from '~/vue_merge_request_widget/components/deployment/deployment_list.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
@@ -25,7 +26,7 @@ describe('MrWidgetPipelineContainer', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200, {});
+ mock.onGet().reply(HTTP_STATUS_OK, {});
});
afterEach(() => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 144e176b0f0..6a899c00b98 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -4,6 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
@@ -39,7 +40,7 @@ describe('MRWidgetPipeline', () => {
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(200, []);
+ const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(HTTP_STATUS_OK, []);
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
@@ -110,6 +111,14 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
+ it('should render the latest downstream pipelines only', () => {
+ // component receives two downstream pipelines. one of them is already outdated
+ // because we retried the trigger job, so the mini pipeline graph will only
+ // render the newly created downstream pipeline instead
+ expect(mockData.pipeline.triggered).toHaveLength(2);
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
+
describe('should render pipeline coverage information', () => {
it('should render coverage percentage', () => {
expect(findPipelineCoverage().text()).toMatch(
@@ -223,7 +232,6 @@ describe('MRWidgetPipeline', () => {
({ pipeline } = JSON.parse(JSON.stringify(mockData)));
pipeline.details.event_type_name = 'Pipeline';
- pipeline.details.name = 'Pipeline';
pipeline.merge_request_event_type = undefined;
pipeline.ref.tag = false;
pipeline.ref.branch = false;
@@ -265,7 +273,6 @@ describe('MRWidgetPipeline', () => {
describe('for a detached merge request pipeline', () => {
it('renders a pipeline widget that reads "Merge request pipeline <ID> <status> for <SHA>"', () => {
pipeline.details.event_type_name = 'Merge request pipeline';
- pipeline.details.name = 'Merge request pipeline';
pipeline.merge_request_event_type = 'detached';
factory();
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 7b52773e92d..ec047fe0714 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -219,18 +219,15 @@ describe('Merge request widget rebase component', () => {
it('renders a message explaining user does not have permissions', () => {
const text = findRebaseMessageText();
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
+ expect(text).toContain('Merge blocked:');
expect(text).toContain('the source branch must be rebased');
});
it('renders the correct target branch name', () => {
- const elem = findRebaseMessage();
+ const text = findRebaseMessageText();
- expect(elem.text()).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('the source branch must be rebased onto the target branch.');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js
new file mode 100644
index 00000000000..436f74d1be2
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js
@@ -0,0 +1,33 @@
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ReportWidgetContainer from '~/vue_merge_request_widget/components/report_widget_container.vue';
+
+describe('app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ slot } = {}) => {
+ wrapper = mountExtended(ReportWidgetContainer, {
+ slots: {
+ default: slot,
+ },
+ });
+ };
+
+ it('hides the container when children has no content', async () => {
+ createComponent({ slot: `<span><b></b></span>` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
+ });
+
+ it('hides the container when children has only empty spaces', async () => {
+ createComponent({ slot: `<span><b>&nbsp;<br/>\t\r\n</b></span>&nbsp;` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
+ });
+
+ it('shows the container when a child has content', async () => {
+ createComponent({ slot: `<span><b>test</b></span>` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index 8eeba4d6274..e4448346685 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
let wrapper;
@@ -23,6 +24,7 @@ describe('Merge request widget merge checks failed state component', () => {
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
- expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]);
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain(MergeChecksFailed.i18n[displayText]);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
index 5c07f4ce143..08700e834d7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetArchived', () => {
let wrapper;
@@ -20,8 +21,8 @@ describe('MRWidgetArchived', () => {
});
it('renders information about merging', () => {
- expect(wrapper.text()).toContain(
- 'Merge unavailable: merge requests are read-only on archived projects.',
- );
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge unavailable:');
+ expect(message).toContain('merge requests are read-only on archived projects.');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
index 774e2bafed3..a6d3a6286a7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
@@ -58,13 +58,8 @@ describe('Commits header component', () => {
expect(findCommitToggle().attributes('aria-label')).toBe('Expand');
});
- it('has a chevron-right icon', async () => {
+ it('has a chevron-right icon', () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ expanded: false });
-
- await nextTick();
expect(findCommitToggle().props('icon')).toBe('chevron-right');
});
@@ -110,25 +105,21 @@ describe('Commits header component', () => {
});
describe('when expanded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ expanded: true });
+ findCommitToggle().trigger('click');
+ await nextTick();
});
- it('toggle has aria-label equal to collapse', async () => {
- await nextTick();
+ it('toggle has aria-label equal to collapse', () => {
expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
});
- it('has a chevron-down icon', async () => {
- await nextTick();
+ it('has a chevron-down icon', () => {
expect(findCommitToggle().props('icon')).toBe('chevron-down');
});
- it('has a collapse text', async () => {
- await nextTick();
+ it('has a collapse text', () => {
expect(findHeaderWrapper().text()).toBe('Collapse');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index a16e4d4a6ea..2ca9dc61745 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -12,9 +12,9 @@ describe('MRWidgetConflicts', () => {
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
const findMergeLocalButton = () => wrapper.findByTestId('merge-locally-button');
- const mergeConflictsText = 'Merge blocked: merge conflicts must be resolved.';
+ const mergeConflictsText = 'merge conflicts must be resolved.';
const fastForwardMergeText =
- 'Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.';
+ 'fast-forward merge is not possible. To merge this request, first rebase locally.';
const userCannotMergeText =
'Users who can write to the source or target branches can resolve the conflicts.';
const resolveConflictsBtnText = 'Resolve conflicts';
@@ -76,8 +76,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(mergeConflictsText);
+ expect(text).not.toContain(userCannotMergeText);
});
it('should not allow you to resolve the conflicts', () => {
@@ -102,8 +103,8 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
@@ -129,8 +130,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(mergeConflictsText);
+ expect(text).not.toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
index 49bd3739fdb..5408f731b34 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import simplePoll from '~/lib/utils/simple_poll';
import MrWidgetMerging from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
jest.mock('~/lib/utils/simple_poll', () =>
jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
@@ -33,14 +34,8 @@ describe('MRWidgetMerging', () => {
});
it('renders information about merge request being merged', () => {
- expect(
- wrapper
- .find('.media-body')
- .text()
- .trim()
- .replace(/\s\s+/g, ' ')
- .replace(/[\r\n]+/g, ' '),
- ).toContain('Merging!');
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merging!');
});
describe('initiateMergePolling', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
index c6e7198c678..42515c597c5 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetNotAllowed', () => {
let wrapper;
@@ -20,8 +21,9 @@ describe('MRWidgetNotAllowed', () => {
});
it('renders informative text', () => {
- expect(wrapper.text()).toContain('Ready to be merged automatically.');
- expect(wrapper.text()).toContain(
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Ready to be merged automatically.');
+ expect(message).toContain(
'Ask someone with write access to this repository to merge this request',
);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 4219ad70b4c..c0197b5e20a 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetPipelineBlocked', () => {
let wrapper;
@@ -20,8 +21,10 @@ describe('MRWidgetPipelineBlocked', () => {
});
it('renders information text', () => {
- expect(wrapper.text()).toBe(
- "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain(
+ "pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
index bd158d59d74..8bae2b62ed1 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,7 +1,9 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { removeBreakLine } from 'helpers/text_helper';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('PipelineFailed', () => {
let wrapper;
@@ -32,16 +34,20 @@ describe('PipelineFailed', () => {
it('should render error message with a disabled merge button', () => {
createComponent();
- expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.');
- expect(wrapper.text()).toContain('Push a commit that fixes the failure');
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('pipeline must succeed');
+ expect(text).toContain('Push a commit that fixes the failure');
expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions');
});
it('should render pipeline blocked message', () => {
createComponent({ isPipelineBlocked: true });
- expect(wrapper.text()).toContain(
- "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain(
+ "pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});
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 d34fc0c1e61..1e4e089e7c1 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
@@ -79,13 +79,10 @@ const createTestService = () => ({
Vue.use(VueApollo);
+let service;
let wrapper;
let readyToMergeResponseSpy;
-const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
-const findPipelineFailedConfirmModal = () =>
- wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
-
const createReadyToMergeResponse = (customMr) => {
return produce(readyToMergeResponse, (draft) => {
Object.assign(draft.data.project.mergeRequest, customMr);
@@ -96,7 +93,7 @@ const createComponent = (customConfig = {}, createState = true) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
mr: createTestMr(customConfig),
- service: createTestService(),
+ service,
},
data() {
if (createState) {
@@ -119,6 +116,13 @@ const createComponent = (customConfig = {}, createState = true) => {
});
};
+const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
+const findMergeImmediatelyDropdown = () =>
+ wrapper.find('[data-testid="merge-immediately-dropdown"');
+const findSourceBranchDeletedText = () =>
+ wrapper.find('[data-testid="source-branch-deleted-text"]');
+const findPipelineFailedConfirmModal = () =>
+ wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
const findCheckboxElement = () => wrapper.findComponent(SquashBeforeMerge);
const findCommitEditElements = () => wrapper.findAllComponents(CommitEdit);
const findCommitDropdownElement = () => wrapper.findComponent(CommitMessageDropdown);
@@ -129,33 +133,20 @@ const findCommitEditWithInputId = (inputId) =>
const findMergeCommitMessage = () => findCommitEditWithInputId('merge-message-edit').props('value');
const findSquashCommitMessage = () =>
findCommitEditWithInputId('squash-message-edit').props('value');
+const findDeleteSourceBranchCheckbox = () =>
+ wrapper.find('[data-testid="delete-source-branch-checkbox"]');
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
+const triggerEditCommitInput = () =>
+ wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
describe('ReadyToMerge', () => {
beforeEach(() => {
+ service = createTestService();
readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(readyToMergeResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
- describe('isAutoMergeAvailable', () => {
- it('should return true when at least one merge strategy is available', () => {
- createComponent({});
-
- expect(wrapper.vm.isAutoMergeAvailable).toBe(true);
- });
-
- it('should return false when no merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
-
- expect(wrapper.vm.isAutoMergeAvailable).toBe(false);
- });
- });
-
describe('status', () => {
it('defaults to success', () => {
createComponent({ mr: { pipeline: true, availableAutoMergeStrategies: [] } });
@@ -190,16 +181,6 @@ describe('ReadyToMerge', () => {
});
});
- describe('Merge Button Variant', () => {
- it('defaults to confirm class', () => {
- createComponent({
- mr: { availableAutoMergeStrategies: [], mergeable: true },
- });
-
- expect(findMergeButton().attributes('variant')).toBe('confirm');
- });
- });
-
describe('status icon', () => {
it('defaults to tick icon', () => {
createComponent({ mr: { mergeable: true } });
@@ -219,334 +200,313 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.iconClass).toEqual('success');
});
});
+ });
- describe('mergeButtonText', () => {
- it('should return "Merge" when no auto merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
+ describe('merge button text', () => {
+ it('should return "Merge" when no auto merge strategies are available', () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
+
+ expect(findMergeButton().text()).toBe('Merge');
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge');
+ it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
+ createComponent({
+ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
});
- it('should return "Merge in progress"', async () => {
- createComponent();
+ expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isMergingImmediately: true });
+ it('should return Merge when pipeline succeeds', () => {
+ createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
- await nextTick();
+ expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ });
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge in progress');
+ describe('merge immediately dropdown', () => {
+ it('dropdown should be hidden if no pipeline is active', () => {
+ createComponent({
+ mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false },
});
- it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
- createComponent({
- mr: { isMergingImmediately: false, preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
- });
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
- });
+ it('dropdown should be hidden if "Pipelines must succeed" is enabled', () => {
+ createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } });
+
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
});
+ });
- describe('autoMergeText', () => {
- it('should return Merge when pipeline succeeds', () => {
- createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
+ describe('merge button disabled state', () => {
+ it('should not be disabled initally', () => {
+ createComponent();
- expect(wrapper.vm.autoMergeText).toEqual('Merge when pipeline succeeds');
- });
+ expect(findMergeButton().props('disabled')).toBe(false);
});
- describe('shouldShowMergeImmediatelyDropdown', () => {
- it('should return false if no pipeline is active', () => {
- createComponent({
- mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false },
- });
+ it('should be disabled when there is no commit message', () => {
+ createComponent({ mr: { commitMessage: '' } });
- expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
+ });
- it('should return false if "Pipelines must succeed" is enabled for the current project', () => {
- createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } });
+ it('should be disabled if merge is not allowed', () => {
+ createComponent({ mr: { preventMerge: true } });
- expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
});
- describe('isMergeButtonDisabled', () => {
- it('should return false with initial data', () => {
- createComponent({ mr: { isMergeAllowed: true, mergeable: false } });
+ it('should be disabled when making request', async () => {
+ createComponent({ mr: { isMergeAllowed: true } }, true);
- expect(wrapper.vm.isMergeButtonDisabled).toBe(false);
- });
+ findMergeButton().vm.$emit('click');
- it('should return true when there is no commit message', () => {
- createComponent({ mr: { isMergeAllowed: true, commitMessage: '' } });
+ await nextTick();
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
+ });
+ });
- it('should return true if merge is not allowed', () => {
+ describe('sourceBranchDeletedText', () => {
+ const should = 'Source branch will be deleted.';
+ const shouldNot = 'Source branch will not be deleted.';
+ const did = 'Deleted the source branch.';
+ const didNot = 'Did not delete the source branch.';
+ const scenarios = [
+ "the MR hasn't merged yet, and the backend-provided value expects to delete the branch",
+ "the MR hasn't merged yet, and the backend-provided value expects to leave the branch",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean falsey value",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean truthy value",
+ 'the MR has been merged, and the backend reports that the branch has been removed',
+ 'the MR has been merged, and the backend reports that the branch has not been removed',
+ 'the MR has been merged, and the backend reports a non-boolean falsey value',
+ 'the MR has been merged, and the backend reports a non-boolean truthy value',
+ ];
+
+ it.each`
+ describe | premerge | mrShould | mrRemoved | output
+ ${scenarios[0]} | ${true} | ${true} | ${null} | ${should}
+ ${scenarios[1]} | ${true} | ${false} | ${null} | ${shouldNot}
+ ${scenarios[2]} | ${true} | ${null} | ${null} | ${shouldNot}
+ ${scenarios[3]} | ${true} | ${'yeah'} | ${null} | ${should}
+ ${scenarios[4]} | ${false} | ${null} | ${true} | ${did}
+ ${scenarios[5]} | ${false} | ${null} | ${false} | ${didNot}
+ ${scenarios[6]} | ${false} | ${null} | ${null} | ${didNot}
+ ${scenarios[7]} | ${false} | ${null} | ${'yep'} | ${did}
+ `(
+ 'in the case that $describe, returns "$output"',
+ ({ premerge, mrShould, mrRemoved, output }) => {
createComponent({
mr: {
- isMergeAllowed: false,
- availableAutoMergeStrategies: [],
- onlyAllowMergeIfPipelineSucceeds: true,
- mergeable: false,
+ state: !premerge ? 'merged' : 'literally-anything-else',
+ shouldRemoveSourceBranch: mrShould,
+ sourceBranchRemoved: mrRemoved,
+ autoMergeEnabled: true,
},
});
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
-
- it('should return true when the vm instance is making request', async () => {
- createComponent({ mr: { isMergeAllowed: true } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isMakingRequest: true });
-
- await nextTick();
-
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
- });
-
- describe('sourceBranchDeletedText', () => {
- const should = 'Source branch will be deleted.';
- const shouldNot = 'Source branch will not be deleted.';
- const did = 'Deleted the source branch.';
- const didNot = 'Did not delete the source branch.';
- const scenarios = [
- "the MR hasn't merged yet, and the backend-provided value expects to delete the branch",
- "the MR hasn't merged yet, and the backend-provided value expects to leave the branch",
- "the MR hasn't merged yet, and the backend-provided value is a non-boolean falsey value",
- "the MR hasn't merged yet, and the backend-provided value is a non-boolean truthy value",
- 'the MR has been merged, and the backend reports that the branch has been removed',
- 'the MR has been merged, and the backend reports that the branch has not been removed',
- 'the MR has been merged, and the backend reports a non-boolean falsey value',
- 'the MR has been merged, and the backend reports a non-boolean truthy value',
- ];
-
- it.each`
- describe | premerge | mrShould | mrRemoved | output
- ${scenarios[0]} | ${true} | ${true} | ${null} | ${should}
- ${scenarios[1]} | ${true} | ${false} | ${null} | ${shouldNot}
- ${scenarios[2]} | ${true} | ${null} | ${null} | ${shouldNot}
- ${scenarios[3]} | ${true} | ${'yeah'} | ${null} | ${should}
- ${scenarios[4]} | ${false} | ${null} | ${true} | ${did}
- ${scenarios[5]} | ${false} | ${null} | ${false} | ${didNot}
- ${scenarios[6]} | ${false} | ${null} | ${null} | ${didNot}
- ${scenarios[7]} | ${false} | ${null} | ${'yep'} | ${did}
- `(
- 'in the case that $describe, returns "$output"',
- ({ premerge, mrShould, mrRemoved, output }) => {
- createComponent({
- mr: {
- state: !premerge ? 'merged' : 'literally-anything-else',
- shouldRemoveSourceBranch: mrShould,
- sourceBranchRemoved: mrRemoved,
- },
- });
-
- expect(wrapper.vm.sourceBranchDeletedText).toBe(output);
- },
- );
- });
+ expect(findSourceBranchDeletedText().text()).toBe(output);
+ },
+ );
});
- describe('methods', () => {
- describe('handleMergeButtonClick', () => {
- const response = (status) => ({
- data: {
- status,
- },
+ describe('Merge Button Variant', () => {
+ it('defaults to confirm class', () => {
+ createComponent({
+ mr: { availableAutoMergeStrategies: [], mergeable: true },
});
- beforeEach(() => {
- readyToMergeResponseSpy = jest
- .fn()
- .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
- .mockResolvedValue(
- createReadyToMergeResponse({
- squash: true,
- squashOnMerge: true,
- defaultMergeCommitMessage: '',
- defaultSquashCommitMessage: '',
- }),
- );
- });
+ expect(findMergeButton().attributes('variant')).toBe('confirm');
+ });
+ });
- it('should handle merge when pipeline succeeds', async () => {
- createComponent();
+ describe('Merge button click', () => {
+ const response = (status) => ({
+ data: {
+ status,
+ },
+ });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest
- .spyOn(wrapper.vm.service, 'merge')
- .mockResolvedValue(response('merge_when_pipeline_succeeds'));
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ removeSourceBranch: false });
+ beforeEach(() => {
+ readyToMergeResponseSpy = jest
+ .fn()
+ .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
+ .mockResolvedValue(
+ createReadyToMergeResponse({
+ squash: true,
+ squashOnMerge: true,
+ defaultMergeCommitMessage: '',
+ defaultSquashCommitMessage: '',
+ }),
+ );
+ });
- wrapper.vm.handleMergeButtonClick(true);
+ it('should handle merge when pipeline succeeds', async () => {
+ createComponent({ mr: { shouldRemoveSourceBranch: false } }, true);
- await waitForPromises();
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
- transition: 'start-auto-merge',
- });
+ findMergeButton().vm.$emit('click');
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ await waitForPromises();
- expect(params).toEqual(
- expect.objectContaining({
- sha: wrapper.vm.mr.sha,
- commit_message: wrapper.vm.mr.commitMessage,
- should_remove_source_branch: false,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
- }),
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-auto-merge',
});
- it('should handle merge failed', async () => {
- createComponent();
+ const params = service.merge.mock.calls[0][0];
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('failed'));
- wrapper.vm.handleMergeButtonClick(false, true);
+ expect(params).toEqual(
+ expect.objectContaining({
+ sha: '12345678',
+ commit_message: commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
+ });
- await waitForPromises();
+ it('should handle merge failed', async () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
- expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('failed'));
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ findMergeButton().vm.$emit('click');
- expect(params.should_remove_source_branch).toBe(true);
- expect(params.auto_merge_strategy).toBeUndefined();
- });
+ await waitForPromises();
- it('should handle merge action accepted case', async () => {
- createComponent();
+ expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
- jest.spyOn(wrapper.vm.mr, 'transitionStateMachine');
- wrapper.vm.handleMergeButtonClick();
+ const params = service.merge.mock.calls[0][0];
- expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
- transition: 'start-merge',
- });
+ expect(params.should_remove_source_branch).toBe(true);
+ expect(params.auto_merge_strategy).toBeUndefined();
+ });
- await waitForPromises();
+ it('should handle merge action accepted case', async () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
- expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
- transition: 'start-merge',
- });
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('success'));
+ jest.spyOn(wrapper.vm.mr, 'transitionStateMachine');
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ findMergeButton().vm.$emit('click');
- expect(params.should_remove_source_branch).toBe(true);
- expect(params.auto_merge_strategy).toBeUndefined();
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-merge',
});
- it('hides edit commit message', async () => {
- createComponent({}, true, true);
+ await waitForPromises();
- await waitForPromises();
+ expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
+ transition: 'start-merge',
+ });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
+ const params = service.merge.mock.calls[0][0];
- await wrapper
- .findComponent('[data-testid="widget_edit_commit_message"]')
- .vm.$emit('input', true);
+ expect(params.should_remove_source_branch).toBe(true);
+ expect(params.auto_merge_strategy).toBeUndefined();
+ });
- expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(true);
+ it('hides edit commit message', async () => {
+ createComponent();
- wrapper.vm.handleMergeButtonClick();
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('success'));
- await waitForPromises();
+ await triggerEditCommitInput();
- expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(false);
- });
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(true);
+
+ findMergeButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(false);
});
+ });
- describe('initiateRemoveSourceBranchPolling', () => {
- it('should emit event and call simplePoll', () => {
- createComponent();
+ describe('initiateRemoveSourceBranchPolling', () => {
+ it('should emit event and call simplePoll', () => {
+ createComponent();
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- wrapper.vm.initiateRemoveSourceBranchPolling();
+ wrapper.vm.initiateRemoveSourceBranchPolling();
- expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
- expect(simplePoll).toHaveBeenCalled();
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
+ expect(simplePoll).toHaveBeenCalled();
});
+ });
- describe('handleRemoveBranchPolling', () => {
- const response = (state) => ({
- data: {
- source_branch_exists: state,
- },
- });
+ describe('handleRemoveBranchPolling', () => {
+ const response = (state) => ({
+ data: {
+ source_branch_exists: state,
+ },
+ });
- it('should call start and stop polling when MR merged', async () => {
- createComponent();
+ it('should call start and stop polling when MR merged', async () => {
+ createComponent();
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(false));
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'poll').mockResolvedValue(response(false));
- let cpc = false; // continuePollingCalled
- let spc = false; // stopPollingCalled
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
- wrapper.vm.handleRemoveBranchPolling(
- () => {
- cpc = true;
- },
- () => {
- spc = true;
- },
- );
+ wrapper.vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(wrapper.vm.service.poll).toHaveBeenCalled();
+ expect(service.poll).toHaveBeenCalled();
- const args = eventHub.$emit.mock.calls[0];
+ const args = eventHub.$emit.mock.calls[0];
- expect(args[0]).toEqual('MRWidgetUpdateRequested');
- expect(args[1]).toBeDefined();
- args[1]();
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).toBeDefined();
+ args[1]();
- expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
- expect(cpc).toBe(false);
- expect(spc).toBe(true);
- });
+ expect(cpc).toBe(false);
+ expect(spc).toBe(true);
+ });
- it('should continue polling until MR is merged', async () => {
- createComponent();
+ it('should continue polling until MR is merged', async () => {
+ createComponent();
- jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(true));
+ jest.spyOn(service, 'poll').mockResolvedValue(response(true));
- let cpc = false; // continuePollingCalled
- let spc = false; // stopPollingCalled
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
- wrapper.vm.handleRemoveBranchPolling(
- () => {
- cpc = true;
- },
- () => {
- spc = true;
- },
- );
+ wrapper.vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(cpc).toBe(true);
- expect(spc).toBe(false);
- });
+ expect(cpc).toBe(true);
+ expect(spc).toBe(false);
});
});
@@ -563,7 +523,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false);
+ expect(findDeleteSourceBranchCheckbox().exists()).toBe(false);
});
});
@@ -575,7 +535,7 @@ describe('ReadyToMerge', () => {
});
it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(wrapper.find('#remove-source-branch-input').props('disabled')).toBe(undefined);
+ expect(findDeleteSourceBranchCheckbox().props('disabled')).toBe(undefined);
});
});
});
@@ -646,7 +606,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should not be rendered if squash before merge is disabled', () => {
@@ -659,7 +619,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should not be rendered if there is only one commit', () => {
@@ -672,7 +632,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should have one edit component if squash is enabled and there is more than 1 commit', async () => {
@@ -686,9 +646,9 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
- expect(findCommitEditElements().length).toBe(1);
+ expect(findCommitEditElements()).toHaveLength(1);
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
});
@@ -702,16 +662,15 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
- expect(findCommitEditElements().length).toBe(2);
+ expect(findCommitEditElements()).toHaveLength(2);
});
- it('should have two edit components when squash is enabled and there is more than 1 commit and mergeRequestWidgetGraphql is enabled', async () => {
+ it('should have two edit components when squash is enabled', async () => {
createComponent(
{
mr: {
- commitsCount: 2,
squashIsSelected: true,
enableSquashBeforeMerge: true,
},
@@ -719,37 +678,9 @@ describe('ReadyToMerge', () => {
true,
);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- loading: false,
- state: {
- ...createTestMr({}),
- userPermissions: {},
- squash: true,
- mergeable: true,
- commitCount: 2,
- commitsWithoutMergeCommits: {},
- },
- });
- await nextTick();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
-
- expect(findCommitEditElements().length).toBe(2);
- });
-
- it('should have one edit components when squash is enabled and there is 1 commit only', async () => {
- createComponent({
- mr: {
- commitsCount: 1,
- squash: true,
- enableSquashBeforeMerge: true,
- },
- });
+ await triggerEditCommitInput();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
-
- expect(findCommitEditElements().length).toBe(1);
+ expect(findCommitEditElements()).toHaveLength(2);
});
it('should have correct edit squash commit label', async () => {
@@ -761,7 +692,7 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
@@ -779,7 +710,7 @@ describe('ReadyToMerge', () => {
mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 },
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findCommitDropdownElement().exists()).toBe(true);
});
@@ -788,7 +719,7 @@ describe('ReadyToMerge', () => {
it('renders a tip including a link to docs on templates', async () => {
createComponent();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findTipLink().exists()).toBe(true);
});
@@ -891,7 +822,8 @@ describe('ReadyToMerge', () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
+ await triggerEditCommitInput();
expect(finderFn()).toBe(initialValue);
});
@@ -899,7 +831,7 @@ describe('ReadyToMerge', () => {
it('should have updated value after graphql refetch', async () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
triggerApprovalUpdated();
await waitForPromises();
@@ -910,7 +842,7 @@ describe('ReadyToMerge', () => {
it('should not update if user has touched', async () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
const input = wrapper.find(inputId);
input.element.value = USER_COMMIT_MESSAGE;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
index 2a343997cf5..aaa4591d67d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -25,7 +25,7 @@ describe('ShaMismatch', () => {
});
it('should render warning message', () => {
- expect(wrapper.element.innerText).toContain(I18N_SHA_MISMATCH.warningMessage);
+ expect(wrapper.text()).toContain('Merge blocked: new changes were just added.');
});
it('action button should have correct label', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
index e2d79c61b9b..c97b42f61ac 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import { removeBreakLine } from 'helpers/text_helper';
import notesEventHub from '~/notes/event_hub';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
@@ -42,7 +43,9 @@ describe('UnresolvedDiscussions', () => {
});
it('should have correct elements', () => {
- expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).toContain('Create issue to resolve all threads');
@@ -54,7 +57,9 @@ describe('UnresolvedDiscussions', () => {
describe('without threads path', () => {
it('should not show create issue link if user cannot create issue', () => {
- expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).not.toContain('Create issue to resolve all threads');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
deleted file mode 100644
index 82aeac1a47d..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { mount } from '@vue/test-utils';
-import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
-
-let wrapper;
-
-const createComponent = (updateMergeRequest = true) => {
- wrapper = mount(WorkInProgress, {
- propsData: {
- mr: {},
- },
- data() {
- return {
- userPermissions: {
- updateMergeRequest,
- },
- };
- },
- });
-};
-
-describe('Merge request widget draft state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('should have correct elements', () => {
- createComponent(true);
-
- expect(wrapper.text()).toContain(
- "Merge blocked: merge request must be marked as ready. It's still marked as draft.",
- );
- expect(wrapper.find('[data-testid="removeWipButton"]').text()).toContain('Mark as ready');
- });
-
- it('should not show removeWIP button is user cannot update MR', () => {
- createComponent(false);
-
- expect(wrapper.find('[data-testid="removeWipButton"]').exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
new file mode 100644
index 00000000000..e610ceb2122
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -0,0 +1,182 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+import { createAlert } from '~/flash';
+import WorkInProgress, {
+ MSG_SOMETHING_WENT_WRONG,
+ MSG_MARK_READY,
+} from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
+import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql';
+import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql';
+import MergeRequest from '~/merge_request';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+
+const TEST_PROJECT_ID = getStateQueryResponse.data.project.id;
+const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id;
+const TEST_MR_IID = '23';
+const TEST_MR_TITLE = 'Test MR Title';
+const TEST_PROJECT_PATH = 'lorem/ipsum';
+
+jest.mock('~/flash');
+jest.mock('~/merge_request');
+
+describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {
+ let wrapper;
+ let apolloProvider;
+
+ let draftQuerySpy;
+ let removeDraftMutationSpy;
+
+ const findWIPButton = () => wrapper.findByTestId('removeWipButton');
+
+ const createDraftQueryResponse = (canUpdateMergeRequest) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ userPermissions: {
+ updateMergeRequest: canUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ });
+ const createRemoveDraftMutationResponse = () => ({
+ data: {
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ draft: false,
+ mergeableDiscussionsState: true,
+ },
+ },
+ },
+ });
+
+ const createComponent = async () => {
+ wrapper = mountExtended(WorkInProgress, {
+ apolloProvider,
+ propsData: {
+ mr: {
+ issuableId: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ iid: TEST_MR_IID,
+ targetProjectFullPath: TEST_PROJECT_PATH,
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ // why: work_in_progress.vue has some coupling that this query has been read before
+ // for some reason this has to happen **after** the component has mounted
+ // or apollo throws errors.
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getStateQuery,
+ variables: {
+ projectPath: TEST_PROJECT_PATH,
+ iid: TEST_MR_IID,
+ },
+ data: getStateQueryResponse.data,
+ });
+ };
+
+ beforeEach(() => {
+ draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true));
+ removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse());
+
+ apolloProvider = createMockApollo([
+ [draftQuery, draftQuerySpy],
+ [removeDraftMutation, removeDraftMutationSpy],
+ ]);
+ });
+
+ describe('when user can update MR', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders text', () => {
+ const message = wrapper.text();
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain('Select Mark as ready to remove it from Draft status.');
+ });
+
+ it('renders mark ready button', () => {
+ expect(findWIPButton().text()).toBe(MSG_MARK_READY);
+ });
+
+ it('does not call remove draft mutation', () => {
+ expect(removeDraftMutationSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when mark ready button is clicked', () => {
+ beforeEach(async () => {
+ findWIPButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('calls mutation spy', () => {
+ expect(removeDraftMutationSpy).toHaveBeenCalledWith({
+ draft: false,
+ iid: TEST_MR_IID,
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
+
+ it('does not create alert', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('calls toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true);
+ });
+ });
+
+ describe('when mutation fails and ready button is clicked', () => {
+ beforeEach(async () => {
+ removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL'));
+ findWIPButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('creates alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: MSG_SOMETHING_WENT_WRONG,
+ });
+ });
+
+ it('does not call toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when user cannot update MR', () => {
+ beforeEach(async () => {
+ draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false));
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render mark ready button', () => {
+ expect(findWIPButton().exists()).toBe(false);
+ });
+ });
+});
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 7e941c5ceaa..973866176c2 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
@@ -7,6 +7,8 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_
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';
+import * as logger from '~/lib/logger';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
createTelemetryHub: jest.fn().mockReturnValue({
@@ -32,7 +34,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
isCollapsible: false,
loadingText: 'Loading widget',
widgetName: 'WidgetTest',
- fetchCollapsedData: () => Promise.resolve([]),
+ fetchCollapsedData: () => Promise.resolve({ headers: {}, status: HTTP_STATUS_OK }),
value: {
collapsed: null,
expanded: null,
@@ -56,7 +58,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('fetches collapsed', async () => {
const fetchCollapsedData = jest
.fn()
- .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+ .mockReturnValue(Promise.resolve({ headers: {}, status: HTTP_STATUS_OK, data: {} }));
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
@@ -83,7 +85,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
const fetchCollapsedData = jest
.fn()
- .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+ .mockReturnValue(Promise.resolve({ headers: {}, status: HTTP_STATUS_OK, data: {} }));
createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
@@ -122,15 +124,23 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
describe('fetch', () => {
it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => {
- const mockData = { headers: {}, status: 200, data: { vulnerabilities: [] } };
+ const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
createComponent({ propsData: { fetchCollapsedData: async () => mockData } });
await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
});
it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => {
- const mockData1 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 1 }] } };
- const mockData2 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 2 }] } };
+ const mockData1 = {
+ headers: {},
+ status: HTTP_STATUS_OK,
+ data: { vulnerabilities: [{ vuln: 1 }] },
+ };
+ const mockData2 = {
+ headers: {},
+ status: HTTP_STATUS_OK,
+ data: { vulnerabilities: [{ vuln: 2 }] },
+ };
createComponent({
propsData: {
@@ -150,6 +160,21 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
});
+ it('throws an error when the handler does not include headers or status objects', async () => {
+ const error = new Error(Widget.MISSING_RESPONSE_HEADERS);
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ jest.spyOn(logger, 'logError').mockImplementation();
+ createComponent({
+ propsData: {
+ fetchCollapsedData: () => Promise.resolve({}),
+ },
+ });
+ await waitForPromises();
+ expect(wrapper.emitted('input')).toBeUndefined();
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ expect(logger.logError).toHaveBeenCalledWith(error.message);
+ });
+
it('calls sentry when failed', async () => {
const error = new Error('Something went wrong');
jest.spyOn(Sentry, 'captureException').mockImplementation();
@@ -279,13 +304,13 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('fetches expanded data when clicked for the first time', async () => {
const mockDataCollapsed = {
headers: {},
- status: 200,
+ status: HTTP_STATUS_OK,
data: { vulnerabilities: [{ vuln: 1 }] },
};
const mockDataExpanded = {
headers: {},
- status: 200,
+ status: HTTP_STATUS_OK,
data: { vulnerabilities: [{ vuln: 2 }] },
};
@@ -377,7 +402,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
isCollapsible: true,
actionButtons: [
{
- fullReport: true,
+ trackFullReportClicked: true,
href: '#',
target: '_blank',
id: 'full-report-button',
diff --git a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
index 16c2adaffaf..e23cd92f53e 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
@@ -82,11 +82,8 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
createComponent({ mockResponse: { data: { project: { id: 'project-id' } } } });
});
- it('displays the correct message', () => {
- expect(wrapper.findByText('Security scans have run').exists()).toBe(true);
- });
-
- it('should not display the artifacts dropdown', () => {
+ it('does not render the widget', () => {
+ expect(wrapper.html()).toBe('');
expect(findDropdown().exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index d9faa7b2d25..13384e1efca 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
@@ -3,6 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
@@ -22,8 +23,6 @@ describe('Terraform extension', () => {
let mock;
const endpoint = '/path/to/terraform/report.json';
- const successStatusCode = 200;
- const errorStatusCode = 500;
const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
@@ -57,7 +56,7 @@ describe('Terraform extension', () => {
describe('while loading', () => {
const loadingText = 'Loading Terraform reports...';
it('should render loading text', async () => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
createComponent();
expect(wrapper.text()).toContain(loadingText);
@@ -68,7 +67,7 @@ describe('Terraform extension', () => {
describe('when the fetching fails', () => {
beforeEach(() => {
- mockPollingApi(errorStatusCode, null, {});
+ mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
return createComponent();
});
@@ -85,7 +84,7 @@ describe('Terraform extension', () => {
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
beforeEach(async () => {
- mockPollingApi(successStatusCode, response, {});
+ mockPollingApi(HTTP_STATUS_OK, response, {});
return createComponent();
});
@@ -102,7 +101,7 @@ describe('Terraform extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
await createComponent();
wrapper.findByTestId('toggle-button').trigger('click');
@@ -164,7 +163,7 @@ describe('Terraform extension', () => {
describe('successful poll', () => {
beforeEach(() => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
return createComponent();
});
@@ -176,7 +175,7 @@ describe('Terraform extension', () => {
describe('polling fails', () => {
beforeEach(() => {
- mockPollingApi(errorStatusCode, null, {});
+ mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
return createComponent();
});
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 20d00a116bb..46e1919b0ea 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -1,5 +1,94 @@
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+export const mockDownstreamPipelinesRest = ({ includeSourceJobRetried = true } = {}) => [
+ {
+ id: 632,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'https://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ coverage: null,
+ source: 'parent_pipeline',
+ source_job: {
+ name: 'bridge_job',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ path: '/kitchen-sink/bakery/-/pipelines/632',
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/kitchen-sink/bakery/-/pipelines/632',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ },
+ project: {
+ id: 21,
+ name: 'bakery',
+ full_path: '/kitchen-sink/bakery',
+ full_name: 'kitchen-sink / bakery',
+ refs_url: '/kitchen-sink/bakery/refs',
+ },
+ },
+ {
+ id: 633,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'https://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ coverage: null,
+ source: 'parent_pipeline',
+ source_job: {
+ name: 'bridge_job',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ path: '/kitchen-sink/bakery/-/pipelines/633',
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/kitchen-sink/bakery/-/pipelines/633',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ },
+ project: {
+ id: 21,
+ name: 'bakery',
+ full_path: '/kitchen-sink/bakery',
+ full_name: 'kitchen-sink / bakery',
+ refs_url: '/kitchen-sink/bakery/refs',
+ },
+ },
+];
+
export const artifacts = [
{
text: 'result.txt',
@@ -207,6 +296,7 @@ export default {
retry_path: '/root/acets-app/pipelines/172/retry',
created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z',
+ triggered: mockDownstreamPipelinesRest(),
},
pipelineCoverageDelta: '15.25',
buildsWithCoverage: [
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 683858b331d..f37276ad594 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
@@ -11,6 +11,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
@@ -74,8 +75,10 @@ describe('MrWidgetOptions', () => {
gon.features = { asyncMrWidget: true };
mock = new MockAdapter(axios);
- mock.onGet(mockData.merge_request_widget_path).reply(() => [200, { ...mockData }]);
- mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, { ...mockData }]);
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [HTTP_STATUS_OK, { ...mockData }]);
+ mock
+ .onGet(mockData.merge_request_cached_widget_path)
+ .reply(() => [HTTP_STATUS_OK, { ...mockData }]);
});
afterEach(() => {
@@ -805,8 +808,8 @@ describe('MrWidgetOptions', () => {
// Override top-level mocked requests, which always use a fresh copy of
// mockData, which always includes the full pipeline object.
- mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
- mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [HTTP_STATUS_OK, mrData]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [HTTP_STATUS_OK, mrData]);
return createComponent(mrData, {
apolloMock: [
@@ -837,7 +840,7 @@ describe('MrWidgetOptions', () => {
describe('suggestPipeline', () => {
beforeEach(() => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
});
describe('given feature flag is enabled', () => {
@@ -986,12 +989,12 @@ describe('MrWidgetOptions', () => {
() =>
Promise.resolve({
headers: { 'poll-interval': 0 },
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
() =>
Promise.resolve({
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
]),
@@ -1009,11 +1012,11 @@ describe('MrWidgetOptions', () => {
() =>
Promise.resolve({
headers: { 'poll-interval': 1 },
- status: 204,
+ status: HTTP_STATUS_NO_CONTENT,
}),
() =>
Promise.resolve({
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
]),
diff --git a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
index 1a109aad911..be31c65b5e2 100644
--- a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
@@ -3,6 +3,11 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
+import {
setEndpoint,
requestArtifacts,
clearEtagPoll,
@@ -61,7 +66,7 @@ describe('Artifacts App Store Actions', () => {
describe('success', () => {
it('dispatches requestArtifacts and receiveArtifactsSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, [
{
text: 'result.txt',
url: 'asda',
@@ -89,7 +94,7 @@ describe('Artifacts App Store Actions', () => {
job_path: 'asda',
},
],
- status: 200,
+ status: HTTP_STATUS_OK,
},
type: 'receiveArtifactsSuccess',
},
@@ -100,7 +105,7 @@ describe('Artifacts App Store Actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestArtifacts and receiveArtifactsError', () => {
@@ -126,7 +131,7 @@ describe('Artifacts App Store Actions', () => {
it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', () => {
return testAction(
receiveArtifactsSuccess,
- { data: { summary: {} }, status: 200 },
+ { data: { summary: {} }, status: HTTP_STATUS_OK },
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
[],
@@ -136,7 +141,7 @@ describe('Artifacts App Store Actions', () => {
it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', () => {
return testAction(
receiveArtifactsSuccess,
- { data: { summary: {} }, status: 204 },
+ { data: { summary: {} }, status: HTTP_STATUS_NO_CONTENT },
mockedState,
[],
[],
diff --git a/spec/frontend/vue_merge_request_widget/test_extensions.js b/spec/frontend/vue_merge_request_widget/test_extensions.js
index 1977f550577..e9e5d931323 100644
--- a/spec/frontend/vue_merge_request_widget/test_extensions.js
+++ b/spec/frontend/vue_merge_request_widget/test_extensions.js
@@ -1,3 +1,4 @@
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export const workingExtension = (shouldCollapse = true) => ({
@@ -120,7 +121,7 @@ export const pollingFullDataExtension = {
return Promise.resolve([
{
headers: { 'poll-interval': 0 },
- status: 200,
+ status: HTTP_STATUS_OK,
data: {
id: 1,
text: 'Hello world',
@@ -152,7 +153,7 @@ export const fullReportExtension = {
text: 'test',
href: `testref`,
target: '_blank',
- fullReport: true,
+ trackFullReportClicked: true,
},
];
},
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 5a0ee5a59ba..98a357bac2b 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
@@ -97,7 +98,7 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(200, mockUsers);
+ mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
@@ -187,7 +188,7 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(200, mockUsers);
+ mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
data: { alert: mockAlert },
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 b7b43264330..ad08120fada 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
@@ -9,6 +9,7 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
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"
+ smooth="0"
tooltiplabel="MB"
/>
</div>
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index e1860d3399b..3f7ec156c19 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,13 +1,13 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { WorkspaceType, IssuableType } from '~/issues/constants';
+import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
const createComponent = ({
workspaceType = WorkspaceType.project,
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
} = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
@@ -28,9 +28,9 @@ describe('ConfidentialityBadge', () => {
});
it.each`
- workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
+ workspaceType | issuableType | expectedTooltip
+ ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
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 0d329b6a065..b0c0fc79676 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
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -35,9 +36,11 @@ describe('MarkdownViewer', () => {
describe('success', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
- body: '<b>testing</b> {{gl_md_img_1}}',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_OK, {
+ body: '<b>testing</b> {{gl_md_img_1}}',
+ });
});
it('renders a skeleton loader while the markdown is loading', () => {
@@ -100,9 +103,11 @@ describe('MarkdownViewer', () => {
describe('error', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(500, {
- body: 'Internal Server Error',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
+ body: 'Internal Server Error',
+ });
});
it('renders an error message if loading the markdown preview fails', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
index 2c5bb86d8a5..c1495e8264a 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
@@ -56,11 +56,11 @@ describe('DateTimePickerInput', () => {
it('input event is emitted when focus is lost', () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
+
const input = wrapper.find('input');
input.setValue(inputValue);
input.trigger('blur');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
+ expect(wrapper.emitted('input')[0][0]).toEqual(inputValue);
});
});
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index f7030f38709..7d8581e11e9 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
describe('DismissibleContainer', () => {
@@ -28,7 +29,7 @@ describe('DismissibleContainer', () => {
});
it('successfully dismisses', () => {
- mockAxios.onPost(propsData.path).replyOnce(200);
+ mockAxios.onPost(propsData.path).replyOnce(HTTP_STATUS_OK);
const button = findBtn();
button.trigger('click');
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index c34041f9305..119d6448507 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -61,27 +61,8 @@ describe('DropdownKeyboardNavigation', () => {
});
describe('keydown events', () => {
- let incrementSpy;
-
beforeEach(() => {
createComponent();
- incrementSpy = jest.spyOn(wrapper.vm, 'increment');
- });
-
- afterEach(() => {
- incrementSpy.mockRestore();
- });
-
- it('onKeydown-Down calls increment(1)', () => {
- helpers.arrowDown();
-
- expect(incrementSpy).toHaveBeenCalledWith(1);
- });
-
- it('onKeydown-Up calls increment(-1)', () => {
- helpers.arrowUp();
-
- expect(incrementSpy).toHaveBeenCalledWith(-1);
});
it('onKeydown-Tab $emits @tab event', () => {
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
new file mode 100644
index 00000000000..6b98f6c5e89
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -0,0 +1,268 @@
+import { nextTick } from 'vue';
+import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import { QUERY_TOO_SHORT_MESSAGE } from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('EntitySelect', () => {
+ let wrapper;
+ let fetchItemsMock;
+ let fetchInitialSelectionTextMock;
+
+ // Mocks
+ const itemMock = {
+ text: 'selectedGroup',
+ value: '1',
+ };
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const headerText = 'headerText';
+ const defaultToggleText = 'defaultToggleText';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {}, slots = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(EntitySelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ headerText,
+ defaultToggleText,
+ fetchItems: fetchItemsMock,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ ...stubs,
+ },
+ slots,
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const selectGroup = async () => {
+ openListbox();
+ await nextTick();
+ findListbox().vm.$emit('select', itemMock.value);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
+ });
+
+ describe('on mount', () => {
+ it('calls the fetch function when the listbox is opened', async () => {
+ createComponent();
+ openListbox();
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("fetches the initially selected value's name", async () => {
+ fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text);
+ createComponent({
+ props: {
+ fetchInitialSelectionText: fetchInitialSelectionTextMock,
+ initialSelection: itemMock.value,
+ },
+ });
+ await nextTick();
+
+ expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+ });
+
+ it("renders the error slot's content", () => {
+ const selector = 'data-test-id="error-element"';
+ createComponent({
+ slots: {
+ error: `<div ${selector} />`,
+ },
+ });
+
+ expect(wrapper.find(`[${selector}]`).exists()).toBe(true);
+ });
+
+ it('renders the label slot if provided', () => {
+ const testid = 'label-slot';
+ createComponent({
+ slots: {
+ label: `<div data-testid="${testid}" />`,
+ },
+ stubs: {
+ GlFormGroup,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', () => {
+ createComponent();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(itemMock.value);
+ expect(findInput().attributes('value')).toBe(itemMock.value);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ createComponent();
+ openListbox();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+
+ fetchItemsMock.mockImplementation(() => ({ items: [], totalPages: 1 }));
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ createComponent();
+ openListbox();
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+
+ describe('pagination', () => {
+ const searchString = 'searchString';
+
+ beforeEach(async () => {
+ let requestCount = 0;
+ fetchItemsMock.mockImplementation((searchQuery, page) => {
+ requestCount += 1;
+ return {
+ items: [
+ {
+ text: `Group [page: ${page} - search: ${searchQuery}]`,
+ value: `id:${requestCount}`,
+ },
+ ],
+ totalPages: 3,
+ };
+ });
+ createComponent();
+ openListbox();
+ findListbox().vm.$emit('bottom-reached');
+ return nextTick();
+ });
+
+ it('fetches the next page when bottom is reached', () => {
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith('', 2);
+ });
+
+ it('fetches the first page when the search query changes', async () => {
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(3);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 1);
+ });
+
+ it('retains the search query when infinite scrolling', async () => {
+ search(searchString);
+ await nextTick();
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(4);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 2);
+ });
+
+ it('pauses infinite scroll after fetching the last page', async () => {
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+ });
+
+ it('resumes infinite scroll when search query changes', async () => {
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
new file mode 100644
index 00000000000..83560e367ea
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -0,0 +1,135 @@
+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';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import GroupSelect from '~/vue_shared/components/entity_select/group_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ GROUP_TOGGLE_TEXT,
+ GROUP_HEADER_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${GROUP_TOGGLE_TEXT}
+ ${'headerText'} | ${GROUP_HEADER_TEXT}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_OK, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: groupMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
new file mode 100644
index 00000000000..57dce032d30
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -0,0 +1,248 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import ProjectSelect from '~/vue_shared/components/entity_select/project_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ PROJECT_TOGGLE_TEXT,
+ PROJECT_HEADER_TEXT,
+ FETCH_PROJECTS_ERROR,
+ FETCH_PROJECT_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('ProjectSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const groupId = '22';
+ const userId = '1';
+
+ // Mocks
+ const apiVersion = 'v4';
+ const projectMock = {
+ name_with_namespace: 'selectedProject',
+ id: '1',
+ };
+ const projectsEndpoint = `/api/${apiVersion}/projects.json`;
+ const groupProjectEndpoint = `/api/${apiVersion}/groups/${groupId}/projects.json`;
+ const userProjectEndpoint = `/api/${apiVersion}/users/${userId}/projects`;
+ const projectEndpoint = `/api/${apiVersion}/projects/${projectMock.id}`;
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = mountExtended(ProjectSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ groupId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeAll(() => {
+ gon.api_version = apiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders HTML label when hasHtmlLabel is true', () => {
+ const testid = 'html-label';
+ createComponent({
+ props: {
+ label: `<div data-testid="${testid}" />`,
+ hasHtmlLabel: true,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT}
+ ${'headerText'} | ${PROJECT_HEADER_TEXT}
+ ${'clearable'} | ${true}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches projects when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].url).toBe(groupProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ include_subgroups: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ with_shared: true,
+ });
+ });
+
+ it('includes projects from subgroups if includeSubgroups is true', async () => {
+ createComponent({
+ props: {
+ includeSubgroups: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.include_subgroups).toBe(true);
+ });
+
+ it('fetches projects globally if no group ID is provided', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(projectsEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ membership: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ });
+ });
+
+ it('restricts search to owned projects if membership is true', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ membership: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.membership).toBe(true);
+ });
+
+ it("fetches the user's projects if a user ID is provided", async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ userId,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(userProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ per_page: 20,
+ search: '',
+ with_shared: true,
+ include_subgroups: false,
+ });
+ });
+
+ it.each([null, groupId])(
+ 'fetches with the provided sort key when groupId is %s',
+ async (groupIdProp) => {
+ const orderBy = 'last_activity_at';
+ createComponent({
+ props: {
+ groupId: groupIdProp,
+ orderBy,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.order_by).toBe(orderBy);
+ },
+ );
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_OK, projectMock);
+ createComponent({ props: { initialSelection: projectMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(projectMock.name_with_namespace);
+ });
+
+ it('show an error if fetching the individual project fails', async () => {
+ mock
+ .onGet(groupProjectEndpoint)
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedProject', id: '2' }]);
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: projectMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECT_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching projects fails', async () => {
+ mock.onGet(groupProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
index 5188e1aabf1..9aa1baf204e 100644
--- a/spec/frontend/vue_shared/components/group_select/utils_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
@@ -1,6 +1,6 @@
-import { groupsPath } from '~/vue_shared/components/group_select/utils';
+import { groupsPath } from '~/vue_shared/components/entity_select/utils';
-describe('group_select utils', () => {
+describe('entity_select utils', () => {
describe('groupsPath', () => {
it.each`
groupsFilter | parentGroupID | expectedPath
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 3f4bfc86b67..0fcc0678c13 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -8,10 +8,7 @@ describe('File Icon component', () => {
const findSvgIcon = () => wrapper.find('svg');
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getIconName = () =>
- findSvgIcon()
- .find('use')
- .element.getAttribute('xlink:href')
- .replace(`${gon.sprite_file_icons}#`, '');
+ findSvgIcon().find('use').element.getAttribute('href').replace(`${gon.sprite_file_icons}#`, '');
const createComponent = (props = {}) => {
wrapper = shallowMount(FileIcon, {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index c3a71d7fda3..b70d4565f56 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -100,15 +100,6 @@ describe('File row component', () => {
});
});
- it('indents row based on level', () => {
- createComponent({
- file: file('t4'),
- level: 2,
- });
-
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
- });
-
it('renders header for file', () => {
createComponent({
file: {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
index 66c6267027b..305f56255a5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
@@ -1,6 +1,7 @@
import { get } from 'lodash';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import mutations from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutations';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -16,7 +17,6 @@ const labels = filterLabels.map(convertObjectPropsToCamelCase);
const filterValue = { value: 'foo' };
describe('Filters mutations', () => {
- const errorCode = 500;
beforeEach(() => {
state = initialState();
});
@@ -79,35 +79,35 @@ describe('Filters mutations', () => {
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'errorCode'} | ${null}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'isLoading'} | ${false}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'data'} | ${[]}
- ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'errorCode'} | ${null}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'data'} | ${[]}
- ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_AUTHORS} | ${'authors'} | ${'isLoading'} | ${true}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'data'} | ${users}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'errorCode'} | ${null}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'data'} | ${[]}
- ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_LABELS} | ${'labels'} | ${'isLoading'} | ${true}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'data'} | ${labels}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'errorCode'} | ${null}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'data'} | ${[]}
- ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_ASSIGNEES} | ${'assignees'} | ${'isLoading'} | ${true}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'data'} | ${users}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'errorCode'} | ${null}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'data'} | ${[]}
- ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state, value);
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
deleted file mode 100644
index 87dd7795b98..00000000000
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import { nextTick } from 'vue';
-import { GlFormGroup, 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';
-import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
-import {
- TOGGLE_TEXT,
- RESET_LABEL,
- FETCH_GROUPS_ERROR,
- FETCH_GROUP_ERROR,
- QUERY_TOO_SHORT_MESSAGE,
-} from '~/vue_shared/components/group_select/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-
-describe('GroupSelect', () => {
- let wrapper;
- let mock;
-
- // Mocks
- const groupMock = {
- full_name: 'selectedGroup',
- id: '1',
- };
- const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
-
- // Stubs
- const GlAlert = {
- template: '<div><slot /></div>',
- };
-
- // Props
- const label = 'label';
- const inputName = 'inputName';
- const inputId = 'inputId';
-
- // Finders
- const findFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const findInput = () => wrapper.findByTestId('input');
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- // Helpers
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(GroupSelect, {
- propsData: {
- label,
- inputName,
- inputId,
- ...props,
- },
- stubs: {
- GlAlert,
- },
- });
- };
- const openListbox = () => findListbox().vm.$emit('shown');
- const search = (searchString) => findListbox().vm.$emit('search', searchString);
- const createComponentWithGroups = () => {
- mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
- createComponent();
- openListbox();
- return waitForPromises();
- };
- const selectGroup = () => {
- findListbox().vm.$emit('select', groupMock.id);
- return nextTick();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('passes the label to GlFormGroup', () => {
- createComponent();
-
- expect(findFormGroup().attributes('label')).toBe(label);
- });
-
- describe('on mount', () => {
- it('fetches groups when the listbox is opened', async () => {
- createComponent();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(0);
-
- openListbox();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- });
-
- describe('with an initial selection', () => {
- it('if the selected group is not part of the fetched list, fetches it individually', async () => {
- mock.onGet(groupEndpoint).reply(200, groupMock);
- createComponent({ props: { initialSelection: groupMock.id } });
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it('show an error if fetching the individual group fails', async () => {
- mock
- .onGet('/api/undefined/groups.json')
- .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
- mock.onGet(groupEndpoint).reply(500);
- createComponent({ props: { initialSelection: groupMock.id } });
-
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
- });
- });
- });
-
- it('shows an error when fetching groups fails', async () => {
- mock.onGet('/api/undefined/groups.json').reply(500);
- createComponent();
- openListbox();
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
- });
-
- describe('selection', () => {
- it('uses the default toggle text while no group is selected', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
-
- describe('once a group is selected', () => {
- it(`uses the selected group's name as the toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it(`uses the selected group's ID as the listbox' and input value`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().attributes('selected')).toBe(groupMock.id);
- expect(findInput().attributes('value')).toBe(groupMock.id);
- });
-
- it(`on reset, falls back to the default toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- findListbox().vm.$emit('reset');
- await nextTick();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
- });
- });
-
- describe('search', () => {
- it('sets `searching` to `true` when first opening the dropdown', async () => {
- createComponent();
-
- expect(findListbox().props('searching')).toBe(false);
-
- openListbox();
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('sets `searching` to `true` while searching', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('searching')).toBe(false);
-
- search('foo');
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('fetches groups matching the search string', async () => {
- const searchString = 'searchString';
- await createComponentWithGroups();
-
- expect(mock.history.get).toHaveLength(1);
-
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('shows a notice if the search query is too short', async () => {
- const searchString = 'a';
- await createComponentWithGroups();
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
- });
- });
-
- describe('pagination', () => {
- const searchString = 'searchString';
-
- beforeEach(async () => {
- let requestCount = 0;
- mock.onGet('/api/undefined/groups.json').reply(({ params }) => {
- requestCount += 1;
- return [
- 200,
- [
- {
- full_name: `Group [page: ${params.page} - search: ${params.search}]`,
- id: requestCount,
- },
- ],
- {
- page: params.page,
- 'x-total-pages': 3,
- },
- ];
- });
- createComponent();
- openListbox();
- findListbox().vm.$emit('bottom-reached');
- return waitForPromises();
- });
-
- it('fetches the next page when bottom is reached', async () => {
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: '',
- });
- });
-
- it('fetches the first page when the search query changes', async () => {
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(3);
- expect(mock.history.get[2].params).toStrictEqual({
- page: 1,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('retains the search query when infinite scrolling', async () => {
- search(searchString);
- await waitForPromises();
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(4);
- expect(mock.history.get[3].params).toStrictEqual({
- page: 2,
- per_page: 20,
- search: searchString,
- });
- });
-
- it('pauses infinite scroll after fetching the last page', async () => {
- expect(findListbox().props('infiniteScroll')).toBe(true);
-
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
- });
-
- it('resumes infinite scroll when search query changes', async () => {
- findListbox().vm.$emit('bottom-reached');
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(false);
-
- search(searchString);
- await waitForPromises();
-
- expect(findListbox().props('infiniteScroll')).toBe(true);
- });
- });
-
- it.each`
- description | clearable | expectedLabel
- ${'passes'} | ${true} | ${RESET_LABEL}
- ${'does not pass'} | ${false} | ${''}
- `(
- '$description the reset button label to the listbox when clearable is $clearable',
- ({ clearable, expectedLabel }) => {
- createComponent({
- props: {
- clearable,
- },
- });
-
- expect(findListbox().props('resetButtonLabel')).toBe(expectedLabel);
- },
- );
-});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 94e1ece8c6b..458f2cc5374 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import HeaderCi from '~/vue_shared/components/header_ci_component.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -28,7 +28,7 @@ describe('Header CI Component', () => {
hasSidebarButton: true,
};
- const findIconBadge = () => wrapper.findComponent(CiIconBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
@@ -59,7 +59,7 @@ describe('Header CI Component', () => {
});
it('should render status badge', () => {
- expect(findIconBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
it('should render timeago date', () => {
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
index 7dca360c7ee..1783538beb3 100644
--- a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
+++ b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
-import IncubationAlert from '~/ml/experiment_tracking/components/incubation_alert.vue';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
describe('IncubationAlert', () => {
let wrapper;
@@ -10,13 +10,20 @@ describe('IncubationAlert', () => {
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
- wrapper = mount(IncubationAlert);
+ wrapper = mount(IncubationAlert, {
+ propsData: {
+ featureName: 'some feature',
+ linkToFeedbackIssue: 'some_link',
+ },
+ });
+ });
+
+ it('displays the feature name in the title', () => {
+ expect(wrapper.html()).toContain('some feature is in incubating phase');
});
it('displays link to issue', () => {
- expect(findButton().attributes().href).toBe(
- 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660',
- );
+ expect(findButton().attributes().href).toBe('some_link');
});
it('is removed if dismissed', async () => {
diff --git a/spec/frontend/vue_shared/components/incubation/pagination_spec.js b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
new file mode 100644
index 00000000000..a621e60c627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
@@ -0,0 +1,76 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+
+describe('~/vue_shared/incubation/components/pagination.vue', () => {
+ let wrapper;
+
+ const pageInfo = {
+ startCursor: 'eyJpZCI6IjE2In0',
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const createWrapper = (pageInfoProp) => {
+ wrapper = mountExtended(Pagination, {
+ propsData: pageInfoProp,
+ });
+ };
+
+ describe('when neither next nor previous page exists', () => {
+ beforeEach(() => {
+ const emptyPageInfo = { ...pageInfo, hasPreviousPage: false, hasNextPage: false };
+
+ createWrapper(emptyPageInfo);
+ });
+
+ it('should not render pagination component', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when Pagination is rendered for environment details page', () => {
+ beforeEach(() => {
+ createWrapper(pageInfo);
+ });
+
+ it('should pass correct props to keyset pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toEqual(expect.objectContaining(pageInfo));
+ });
+
+ describe.each([
+ {
+ testPageInfo: pageInfo,
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: true, hasPreviousPage: false },
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: '',
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: false, hasPreviousPage: true },
+ expectedAfter: '',
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ ])(
+ 'button links generation for $testPageInfo',
+ ({ testPageInfo, expectedAfter, expectedBefore }) => {
+ beforeEach(() => {
+ createWrapper(testPageInfo);
+ });
+
+ it(`should have button links defined as ${expectedAfter || 'empty'} and
+ ${expectedBefore || 'empty'}`, () => {
+ expect(findPagination().props().prevButtonLink).toContain(expectedBefore);
+ expect(findPagination().props().nextButtonLink).toContain(expectedAfter);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 3b8e78bbadd..68ce07f86b9 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,11 +1,13 @@
+import $ from 'jquery';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
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 { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -74,6 +76,22 @@ describe('Markdown field component', () => {
);
}
+ function createWrapper({ autocompleteDataSources = {} } = {}) {
+ subject = shallowMountExtended(MarkdownField, {
+ propsData: {
+ markdownDocsPath,
+ markdownPreviewPath,
+ isSubmitting: false,
+ textareaValue,
+ lines: [],
+ enablePreview: true,
+ restrictedToolBarItems,
+ showContentEditorSwitcher: false,
+ autocompleteDataSources,
+ },
+ });
+ }
+
const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
@@ -84,6 +102,7 @@ describe('Markdown field component', () => {
const findDropzone = () => subject.find('.div-dropzone');
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
+ const findGlForm = () => $(subject.vm.$refs['gl-form']).data('glForm');
describe('mounted', () => {
const previewHTML = `
@@ -100,6 +119,18 @@ describe('Markdown field component', () => {
findDropzone().element.addEventListener('click', dropzoneSpy);
});
+ describe('GlForm', () => {
+ beforeEach(() => {
+ createWrapper({ autocompleteDataSources: { commands: '/foobar/-/autocomplete_sources' } });
+ });
+
+ it('initializes GlForm with autocomplete data sources', () => {
+ expect(findGlForm().autoComplete.dataSources).toMatchObject({
+ commands: '/foobar/-/autocomplete_sources',
+ });
+ });
+ });
+
it('renders textarea inside backdrop', () => {
expect(subject.find('.zen-backdrop textarea').element).not.toBeNull();
});
@@ -107,7 +138,7 @@ describe('Markdown field component', () => {
it('renders referenced commands on markdown preview', async () => {
axiosMock
.onPost(markdownPreviewPath)
- .reply(200, { references: { users: [], commands: 'test command' } });
+ .reply(HTTP_STATUS_OK, { references: { users: [], commands: 'test command' } });
previewLink = getPreviewLink();
previewLink.vm.$emit('click', { target: {} });
@@ -121,7 +152,7 @@ describe('Markdown field component', () => {
describe('markdown preview', () => {
beforeEach(() => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { body: previewHTML });
});
it('sets preview link as active', async () => {
@@ -267,7 +298,7 @@ describe('Markdown field component', () => {
const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
it('shows warning on mention of all users', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -279,7 +310,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -298,7 +329,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed while endpoint is loading', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
jest.spyOn(axios, 'post');
subject.setProps({ textareaValue: 'hello @all' });
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 e3df2cde1c1..26b536984ff 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -24,6 +24,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldName = 'form[markdown_field]';
const formFieldPlaceholder = 'Write some markdown';
const formFieldAriaLabel = 'Edit your content';
+ const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
@@ -35,11 +36,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
markdownDocsPath,
quickActionsDocsPath,
enableAutocomplete,
+ autocompleteDataSources,
enablePreview,
- formFieldId,
- formFieldName,
- formFieldPlaceholder,
- formFieldAriaLabel,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
...propsData,
},
stubs: {
@@ -66,18 +70,17 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('displays markdown field by default', () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
- expect(findMarkdownField().props()).toEqual(
- expect.objectContaining({
- markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
- canAttachFile: true,
- enableAutocomplete,
- textareaValue: value,
- markdownDocsPath,
- uploadsPath: window.uploads_path,
- enablePreview,
- }),
- );
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources,
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ });
});
it('renders markdown field textarea', () => {
@@ -95,6 +98,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findTextarea().element.value).toBe(value);
});
+ it('fails to render if textarea id and name is not passed', () => {
+ expect(() => {
+ buildWrapper({ propsData: { formFieldProps: {} } });
+ }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"');
+ });
+
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
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 adcf57b76a4..c1e61f6e43d 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
@@ -4,6 +4,7 @@ import {
splitDocument,
} from '~/vue_shared/components/markdown_drawer/utils/fetch';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
MOCK_HTML,
MOCK_DRAWER_DATA,
@@ -20,9 +21,9 @@ describe('utils/fetch', () => {
});
describe.each`
- axiosMock | type | toExpect
- ${{ code: 200, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
- ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ axiosMock | type | toExpect
+ ${{ code: HTTP_STATUS_OK, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: HTTP_STATUS_INTERNAL_SERVER_ERROR, 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/new_resource_dropdown/mock_data.js b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
new file mode 100644
index 00000000000..19b1453e8ac
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
@@ -0,0 +1,54 @@
+export const emptySearchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [],
+ },
+ },
+};
+
+export const emptySearchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: emptySearchProjectsQueryResponse.data.projects,
+ },
+ },
+};
+
+export const project1 = {
+ id: 'gid://gitlab/Group/26',
+ name: 'Super Mario Project',
+ nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
+};
+
+export const project2 = {
+ id: 'gid://gitlab/Group/59',
+ name: 'Mario Kart Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
+};
+
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
+export const searchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [project1, project2, project3],
+ },
+ },
+};
+
+export const searchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: searchProjectsQueryResponse.data.projects,
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
new file mode 100644
index 00000000000..31320b1d2a6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -0,0 +1,262 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+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 NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
+import searchUserProjectsWithIssuesEnabledQuery from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql';
+import { RESOURCE_TYPES } from '~/vue_shared/components/new_resource_dropdown/constants';
+import searchProjectsWithinGroupQuery from '~/issues/list/queries/search_projects.query.graphql';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import {
+ emptySearchProjectsQueryResponse,
+ emptySearchProjectsWithinGroupQueryResponse,
+ project1,
+ project2,
+ project3,
+ searchProjectsQueryResponse,
+ searchProjectsWithinGroupQueryResponse,
+} from './mock_data';
+
+jest.mock('~/flash');
+
+describe('NewResourceDropdown component', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ // Props
+ const withinGroupProps = {
+ query: searchProjectsWithinGroupQuery,
+ queryVariables: { fullPath: 'mushroom-kingdom' },
+ extractProjects: (data) => data.group.projects.nodes,
+ };
+
+ const mountComponent = ({
+ search = '',
+ query = searchUserProjectsWithIssuesEnabledQuery,
+ queryResponse = searchProjectsQueryResponse,
+ mountFn = shallowMount,
+ propsData = {},
+ } = {}) => {
+ const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewResourceDropdown, {
+ apolloProvider,
+ propsData,
+ data() {
+ return { search };
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('shown');
+ await waitForPromises();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ };
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('renders a split dropdown', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it('renders a label for the dropdown toggle button', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().attributes('toggle-text')).toBe(
+ NewResourceDropdown.i18n.toggleButtonLabel,
+ );
+ });
+
+ it('focuses on input when dropdown is shown', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
+
+ await showDropdown();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe.each`
+ description | propsData | query | queryResponse | emptyResponse
+ ${'by default'} | ${undefined} | ${searchUserProjectsWithIssuesEnabledQuery} | ${searchProjectsQueryResponse} | ${emptySearchProjectsQueryResponse}
+ ${'within a group'} | ${withinGroupProps} | ${searchProjectsWithinGroupQuery} | ${searchProjectsWithinGroupQueryResponse} | ${emptySearchProjectsWithinGroupQueryResponse}
+ `('$description', ({ propsData, query, queryResponse, emptyResponse }) => {
+ it('renders projects options', async () => {
+ wrapper = mountComponent({ mountFn: mount, query, queryResponse, propsData });
+ await showDropdown();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ expect(listItems.at(2).text()).toBe(project3.nameWithNamespace);
+ });
+
+ it('renders `No matches found` when there are no matches', async () => {
+ wrapper = mountComponent({
+ search: 'no matches',
+ query,
+ queryResponse: emptyResponse,
+ mountFn: mount,
+ propsData,
+ });
+
+ await showDropdown();
+
+ expect(wrapper.find('li').text()).toBe(NewResourceDropdown.i18n.noMatchesFound);
+ });
+
+ describe.each`
+ resourceType | expectedDefaultLabel | expectedPath | expectedLabel
+ ${'issue'} | ${'Select project to create issue'} | ${'issues/new'} | ${'New issue in'}
+ ${'merge-request'} | ${'Select project to create merge request'} | ${'merge_requests/new'} | ${'New merge request in'}
+ ${'milestone'} | ${'Select project to create milestone'} | ${'milestones/new'} | ${'New milestone in'}
+ `(
+ 'with resource type $resourceType',
+ ({ resourceType, expectedDefaultLabel, expectedPath, expectedLabel }) => {
+ describe('when no project is selected', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ });
+
+ it('dropdown button is not a link', () => {
+ expect(findDropdown().attributes('split-href')).toBeUndefined();
+ });
+
+ it('displays default text on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(expectedDefaultLabel);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ await showDropdown();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ });
+
+ it('dropdown button is a link', () => {
+ const href = joinPaths(project1.webUrl, DASH_SCOPE, expectedPath);
+
+ expect(findDropdown().attributes('split-href')).toBe(href);
+ });
+
+ it('displays project name on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(`${expectedLabel} ${project1.name}`);
+ });
+ });
+ },
+ );
+ });
+
+ describe('without localStorage', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ mountFn: mount });
+ });
+
+ it('does not attempt to save the selected project to the localStorage', async () => {
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with localStorage', () => {
+ it('retrieves the selected project from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ webUrl: project1.webUrl,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ it('retrieves legacy cache from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ url: `${project1.webUrl}/issues/new`,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ describe.each(RESOURCE_TYPES)('with resource type %s', (resourceType) => {
+ it('computes the local storage key without a group', async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group--new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+
+ it('computes the local storage key with a group', async () => {
+ const groupId = '22';
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { groupId, resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group-${groupId}-new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+ });
+ });
+});
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 559f9bcb1a8..bcfd7a8ec70 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -4,6 +4,7 @@ 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 { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -85,7 +86,7 @@ describe('system note component', () => {
it('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
deleted file mode 100644
index c8ca75787f1..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { s__ } from '~/locale';
-import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
-import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- visitUrl: jest.fn(),
-}));
-
-const mockModalId = 'runner-aws-deployments-modal';
-
-describe('RunnerAwsDeploymentsModal', () => {
- let wrapper;
-
- const findModal = () => wrapper.findComponent(GlModal);
- const findRunnerAwsInstructions = () => wrapper.findComponent(RunnerAwsInstructions);
-
- const createComponent = (options) => {
- wrapper = shallowMount(RunnerAwsDeploymentsModal, {
- propsData: {
- modalId: mockModalId,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders modal', () => {
- expect(findModal().props()).toMatchObject({
- size: 'sm',
- modalId: mockModalId,
- title: s__('Runners|Deploy GitLab Runner in AWS'),
- });
- expect(findModal().attributes()).toMatchObject({
- 'hide-footer': '',
- });
- });
-
- it('renders modal contents', () => {
- expect(findRunnerAwsInstructions().exists()).toBe(true);
- });
-
- it('when contents trigger closing, modal closes', () => {
- const mockClose = jest.fn();
-
- createComponent({
- stubs: {
- GlModal: {
- template: '<div><slot/></div>',
- methods: {
- close: mockClose,
- },
- },
- },
- });
-
- expect(mockClose).toHaveBeenCalledTimes(0);
-
- findRunnerAwsInstructions().vm.$emit('close');
-
- expect(mockClose).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
deleted file mode 100644
index 639668761ea..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import RunnerAwsDeployments from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue';
-import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
-
-describe('RunnerAwsDeployments component', () => {
- let wrapper;
-
- const findModalButton = () => wrapper.findByTestId('show-modal-button');
- const findModal = () => wrapper.findComponent(RunnerAwsDeploymentsModal);
-
- const createComponent = () => {
- wrapper = extendedWrapper(shallowMount(RunnerAwsDeployments));
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should show the "Deploy GitLab Runner in AWS" button', () => {
- expect(findModalButton().exists()).toBe(true);
- expect(findModalButton().text()).toBe('Deploy GitLab Runner in AWS');
- });
-
- it('should not render the modal once mounted', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('should render the modal once clicked', async () => {
- findModalButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findModal().exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
index 4d566dbec0c..6d8f895a185 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
@@ -16,14 +16,18 @@ import {
AWS_TEMPLATES_BASE_URL,
AWS_EASY_BUTTONS,
} from '~/vue_shared/components/runner_instructions/constants';
-import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
import { __ } from '~/locale';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
+const mockRegistrationToken = 'MY_TOKEN';
+
describe('RunnerAwsInstructions', () => {
let wrapper;
@@ -31,6 +35,7 @@ describe('RunnerAwsInstructions', () => {
const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio);
const findEasyButtonAt = (i) => findEasyButtons().at(i);
const findLink = () => wrapper.findComponent(GlLink);
+ const findModalCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findOkButton = () =>
wrapper
.findAllComponents(GlButton)
@@ -38,8 +43,12 @@ describe('RunnerAwsInstructions', () => {
.at(0);
const findCloseButton = () => wrapper.findByText(__('Close'));
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(RunnerAwsInstructions, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
stubs: {
GlSprintf,
},
@@ -109,6 +118,22 @@ describe('RunnerAwsInstructions', () => {
expect(findLink().attributes('href')).toBe(AWS_README_URL);
});
+ it('shows registration token and copy button', () => {
+ const token = wrapper.findByText(mockRegistrationToken);
+
+ expect(token.exists()).toBe(true);
+ expect(token.element.tagName).toBe('PRE');
+
+ expect(findModalCopyButton().props('text')).toBe(mockRegistrationToken);
+ });
+
+ it('does not show registration token and copy button when token is not present', () => {
+ createComponent({ props: { registrationToken: null } });
+
+ expect(wrapper.find('pre').exists()).toBe(false);
+ expect(findModalCopyButton().exists()).toBe(false);
+ });
+
it('triggers the modal to close', () => {
findCloseButton().vm.$emit('click');
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 19f2dd137ff..8f593b6aa1b 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
@@ -11,6 +11,7 @@ import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions
import RunnerCliInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue';
import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
import { mockRunnerPlatforms } from './mock_data';
@@ -156,6 +157,7 @@ describe('RunnerInstructionsModal component', () => {
platform | component
${'docker'} | ${RunnerDockerInstructions}
${'kubernetes'} | ${RunnerKubernetesInstructions}
+ ${'aws'} | ${RunnerAwsInstructions}
`('with platform "$platform"', ({ platform, component }) => {
beforeEach(async () => {
createComponent({ props: { defaultPlatformName: platform } });
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
index 1e08394dd56..66d27b5d605 100644
--- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
+++ b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
@@ -22,7 +22,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
1 High
@@ -55,7 +55,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -98,7 +98,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -108,7 +108,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
2 High
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
new file mode 100644
index 00000000000..26c9a6f8d5a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
+<div
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ data-testid="line-numbers"
+>
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ href="some/blame/path.js#L71"
+ />
+
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ data-line-number="71"
+ href="#L71"
+ id="L71"
+ >
+
+ 71
+
+ </a>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
new file mode 100644
index 00000000000..da9067a8ddc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
@@ -0,0 +1,123 @@
+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_deprecated.vue';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
+
+const DEFAULT_PROPS = {
+ chunkIndex: 2,
+ isHighlighted: false,
+ content: '// Line 1 content \n // Line 2 content',
+ startingFrom: 140,
+ totalLines: 50,
+ language: 'javascript',
+ blamePath: 'blame/file.js',
+};
+
+const hash = '#L142';
+
+describe('Chunk component', () => {
+ let wrapper;
+ let idleCallbackSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
+ });
+ };
+
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findContent = () => wrapper.findByTestId('content');
+
+ beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
+ createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('Intersection observer', () => {
+ it('renders an Intersection observer component', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+ });
+
+ it('emits an appear event when intersection-observer appears', () => {
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
+ });
+
+ it('does not emit an appear event is isHighlighted is true', () => {
+ createComponent({ isHighlighted: true });
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual(undefined);
+ });
+ });
+
+ describe('rendering', () => {
+ it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().exists()).toBe(true);
+ });
+
+ it('does not render a Chunk Line component if isHighlighted is false', () => {
+ expect(findChunkLines().length).toBe(0);
+ });
+
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent();
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
+ it('renders simplified line numbers and content if isHighlighted is false', () => {
+ expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
+
+ expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ });
+
+ it('renders Chunk Line components if isHighlighted is true', () => {
+ const splitContent = DEFAULT_PROPS.content.split('\n');
+ createComponent({ isHighlighted: true });
+
+ expect(findChunkLines().length).toBe(splitContent.length);
+
+ expect(findChunkLines().at(0).props()).toMatchObject({
+ number: DEFAULT_PROPS.startingFrom + 1,
+ content: splitContent[0],
+ language: DEFAULT_PROPS.language,
+ blamePath: DEFAULT_PROPS.blamePath,
+ });
+ });
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', async () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
+});
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 657bd59dac6..95ef11d776a 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
@@ -2,27 +2,7 @@ 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 LineHighlighter from '~/blob/line_highlighter';
-
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () =>
- jest.fn().mockReturnValue({
- highlightHash: jest.fn(),
- }),
-);
-
-const DEFAULT_PROPS = {
- chunkIndex: 2,
- isHighlighted: false,
- content: '// Line 1 content \n // Line 2 content',
- startingFrom: 140,
- totalLines: 50,
- language: 'javascript',
- blamePath: 'blame/file.js',
-};
-
-const hash = '#L142';
+import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
@@ -30,14 +10,13 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
- mocks: { $route: { hash } },
- propsData: { ...DEFAULT_PROPS, ...props },
+ propsData: { ...CHUNK_1, ...props },
+ provide: { glFeatures: { fileLineBlame: true } },
});
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
- const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
@@ -52,72 +31,57 @@ describe('Chunk component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
- it('emits an appear event when intersection-observer appears', () => {
+ it('renders highlighted content if appear event is emitted', async () => {
+ createComponent({ chunkIndex: 1, isHighlighted: false });
findIntersectionObserver().vm.$emit('appear');
- expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
- });
-
- it('does not emit an appear event is isHighlighted is true', () => {
- createComponent({ isHighlighted: true });
- findIntersectionObserver().vm.$emit('appear');
+ await nextTick();
- expect(wrapper.emitted('appear')).toEqual(undefined);
+ expect(findContent().exists()).toBe(true);
});
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
jest.clearAllMocks();
- createComponent({ isFirstChunk: true });
expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().exists()).toBe(true);
- });
-
- it('does not render a Chunk Line component if isHighlighted is false', () => {
- expect(findChunkLines().length).toBe(0);
+ expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
- it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ it('does not render content if browser is not in idle state', () => {
idleCallbackSpy.mockRestore();
- createComponent();
+ createComponent({ chunkIndex: 1, ...CHUNK_2 });
expect(findLineNumbers()).toHaveLength(0);
expect(findContent().exists()).toBe(false);
});
- it('renders simplified line numbers and content if isHighlighted is false', () => {
- expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+ describe('isHighlighted is false', () => {
+ beforeEach(() => createComponent(CHUNK_2));
- expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
+ it('does not render line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(0);
+ });
- expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ it('renders raw content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.rawContent);
+ });
});
- it('renders Chunk Line components if isHighlighted is true', () => {
- const splitContent = DEFAULT_PROPS.content.split('\n');
- createComponent({ isHighlighted: true });
+ describe('isHighlighted is true', () => {
+ beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
- expect(findChunkLines().length).toBe(splitContent.length);
+ it('renders line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
- expect(findChunkLines().at(0).props()).toMatchObject({
- number: DEFAULT_PROPS.startingFrom + 1,
- content: splitContent[0],
- language: DEFAULT_PROPS.language,
- blamePath: DEFAULT_PROPS.blamePath,
+ // Opted for a snapshot test here since the output is simple and verifies native HTML elements
+ expect(findLineNumbers().at(0).element).toMatchSnapshot();
});
- });
- it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(LineHighlighter).not.toHaveBeenCalled();
- });
-
- it('scrolls to route hash if last chunk is loaded', async () => {
- createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- await nextTick();
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ it('renders highlighted content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
index 4a995e2fde1..d2dd4afe09e 100644
--- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -1,15 +1,10 @@
-import hljs from 'highlight.js/lib/core';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import hljs from 'highlight.js';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants';
-jest.mock('highlight.js/lib/core', () => ({
- highlight: jest.fn().mockReturnValue({}),
- registerLanguage: jest.fn(),
-}));
-
-jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
- javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+jest.mock('highlight.js', () => ({
+ highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }),
}));
jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
@@ -17,28 +12,61 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
}));
const fileType = 'text';
-const content = 'function test() { return true };';
+const rawContent = 'function test() { return true }; \n // newline';
+const highlightedContent = 'highlighted content';
const language = 'javascript';
describe('Highlight utility', () => {
- beforeEach(() => highlight(fileType, content, language));
-
- it('loads the language', () => {
- expect(languageLoader.javascript).toHaveBeenCalled();
- });
+ beforeEach(() => highlight(fileType, rawContent, language));
it('registers the plugins', () => {
expect(registerPlugins).toHaveBeenCalled();
});
- it('registers the language', () => {
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- language,
- languageLoader[language]().default,
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(rawContent, { language });
+ });
+
+ it('splits the content into chunks', () => {
+ const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
+
+ const chunks = [
+ {
+ language,
+ highlightedContent,
+ rawContent: contentArray.slice(0, 70).join(NEWLINE), // first 70 lines
+ startingFrom: 0,
+ totalLines: LINES_PER_CHUNK,
+ },
+ {
+ language,
+ highlightedContent: '',
+ rawContent: contentArray.slice(70, 140).join(NEWLINE), // last 70 lines
+ startingFrom: 70,
+ totalLines: LINES_PER_CHUNK,
+ },
+ ];
+
+ expect(highlight(fileType, contentArray.join(NEWLINE), language)).toEqual(
+ expect.arrayContaining(chunks),
);
});
+});
- it('highlights the content', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+describe('unsupported languages', () => {
+ const unsupportedLanguage = 'some_unsupported_language';
+
+ beforeEach(() => highlight(fileType, rawContent, unsupportedLanguage));
+
+ it('does not register plugins', () => {
+ expect(registerPlugins).not.toHaveBeenCalled();
+ });
+
+ it('does not attempt to highlight the content', () => {
+ expect(hljs.highlight).not.toHaveBeenCalled();
+ });
+
+ it('does not return a result', () => {
+ expect(highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
new file mode 100644
index 00000000000..f35e9607d5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -0,0 +1,24 @@
+const path = 'some/path.js';
+const blamePath = 'some/blame/path.js';
+
+export const LANGUAGE_MOCK = 'docker';
+
+export const BLOB_DATA_MOCK = { language: LANGUAGE_MOCK, path, blamePath };
+
+export const CHUNK_1 = {
+ isHighlighted: true,
+ rawContent: 'chunk 1 raw',
+ highlightedContent: 'chunk 1 highlighted',
+ totalLines: 70,
+ startingFrom: 0,
+ blamePath,
+};
+
+export const CHUNK_2 = {
+ isHighlighted: false,
+ rawContent: 'chunk 2 raw',
+ highlightedContent: 'chunk 2 highlighted',
+ totalLines: 40,
+ startingFrom: 70,
+ blamePath,
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
new file mode 100644
index 00000000000..0beec8e9d3e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
@@ -0,0 +1,177 @@
+import hljs from 'highlight.js/lib/core';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
+import Tracking from '~/tracking';
+
+jest.mock('~/blob/line_highlighter');
+jest.mock('highlight.js/lib/core');
+jest.mock('~/vue_shared/components/source_viewer/plugins/index');
+Vue.use(VueRouter);
+const router = new VueRouter();
+
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
+ let generatedContent = '';
+ for (let i = 0; i < totalLines; i += 1) {
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
+ }
+ return generatedContent;
+};
+
+const execImmediately = (callback) => callback();
+
+describe('Source Viewer component', () => {
+ let wrapper;
+ const language = 'docker';
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
+ const chunk1 = generateContent('// Some source code 1', 70);
+ const chunk2 = generateContent('// Some source code 2', 70);
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
+ const path = 'some/path.js';
+ const blamePath = 'some/blame/path.js';
+ const fileType = 'javascript';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+
+ const createComponent = async (blob = {}) => {
+ wrapper = shallowMountExtended(SourceViewer, {
+ router,
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ });
+ await waitForPromises();
+ };
+
+ const findChunks = () => wrapper.findAllComponents(Chunk);
+
+ beforeEach(() => {
+ hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
+ hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ jest.spyOn(eventHub, '$emit');
+ jest.spyOn(Tracking, 'event');
+
+ return createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('does not emit an error event when the language is supported', () => {
+ expect(wrapper.emitted('error')).toBeUndefined();
+ });
+
+ it('fires a tracking event and emits an error when the language is not supported', () => {
+ const unsupportedLanguage = 'apex';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ describe('legacy fallbacks', () => {
+ it('tracks a fallback event and emits an error when viewing python files', () => {
+ const fallbackLanguage = 'python';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
+ createComponent({ language: fallbackLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ describe('highlight.js', () => {
+ beforeEach(() => createComponent({ language: mappedLanguage }));
+
+ it('registers our plugins for Highlight.js', () => {
+ expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
+ });
+
+ it('registers the language definition', async () => {
+ const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ mappedLanguage,
+ languageDefinition.default,
+ );
+ });
+
+ it('registers json language definition if fileType is package_json', async () => {
+ await createComponent({ language: 'json', fileType: 'package_json' });
+ const languageDefinition = await import(`highlight.js/lib/languages/json`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
+ });
+
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Ruby' });
+ const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
+ });
+
+ it('highlights the first chunk', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
+ });
+
+ describe('auto-detects if a language cannot be loaded', () => {
+ beforeEach(() => createComponent({ language: 'some_unknown_language' }));
+
+ it('highlights the content with auto-detection', () => {
+ expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
+ });
+ });
+ });
+
+ describe('rendering', () => {
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
+ });
+ });
+
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
+ });
+ });
+
+ describe('LineHighlighter', () => {
+ it('instantiates the lineHighlighter class', async () => {
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 5461d38599d..1c75442b4a8 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,70 +1,27 @@
-import hljs from 'highlight.js/lib/core';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
-} from '~/vue_shared/components/source_viewer/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
-jest.mock('~/blob/line_highlighter');
-jest.mock('highlight.js/lib/core');
-jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-Vue.use(VueRouter);
-const router = new VueRouter();
-
-const generateContent = (content, totalLines = 1, delimiter = '\n') => {
- let generatedContent = '';
- for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
- }
- return generatedContent;
-};
-
-const execImmediately = (callback) => callback();
+jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
- const language = 'docker';
- const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const chunk1 = generateContent('// Some source code 1', 70);
- const chunk2 = generateContent('// Some source code 2', 70);
- const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
- const chunk3Result = generateContent('// Some source code 3', 70, '\n');
- const content = chunk1 + chunk2 + chunk3;
- const path = 'some/path.js';
- const blamePath = 'some/blame/path.js';
- const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
- const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
- const createComponent = async (blob = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(SourceViewer, {
- router,
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
});
- await waitForPromises();
};
const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(eventHub, '$emit');
jest.spyOn(Tracking, 'event');
-
return createComponent();
});
@@ -72,106 +29,19 @@ describe('Source Viewer component', () => {
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
});
- it('does not emit an error event when the language is supported', () => {
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('fires a tracking event and emits an error when the language is not supported', () => {
- const unsupportedLanguage = 'apex';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
- createComponent({ language: unsupportedLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('legacy fallbacks', () => {
- it('tracks a fallback event and emits an error when viewing python files', () => {
- const fallbackLanguage = 'python';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
- createComponent({ language: fallbackLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('highlight.js', () => {
- beforeEach(() => createComponent({ language: mappedLanguage }));
-
- it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
- });
-
- it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- mappedLanguage,
- languageDefinition.default,
- );
- });
-
- it('registers json language definition if fileType is package_json', async () => {
- await createComponent({ language: 'json', fileType: 'package_json' });
- const languageDefinition = await import(`highlight.js/lib/languages/json`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
- });
-
- it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Ruby' });
- const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
- });
-
- it('highlights the first chunk', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
- expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
- });
-
- describe('auto-detects if a language cannot be loaded', () => {
- beforeEach(() => createComponent({ language: 'some_unknown_language' }));
-
- it('highlights the content with auto-detection', () => {
- expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
- });
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
});
});
describe('rendering', () => {
- it.each`
- chunkIndex | chunkContent | totalChunks
- ${0} | ${chunk1} | ${0}
- ${1} | ${chunk2} | ${3}
- ${2} | ${chunk3Result} | ${3}
- `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
- const chunk = findChunks().at(chunkIndex);
-
- expect(chunk.props('content')).toContain(chunkContent.trim());
-
- expect(chunk.props()).toMatchObject({
- totalLines: LINES_PER_CHUNK,
- startingFrom: LINES_PER_CHUNK * chunkIndex,
- totalChunks,
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
- });
- });
-
- describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', async () => {
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
});
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index acda1a64a75..30a7439579f 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { historyPushState } from '~/lib/utils/common_utils';
+import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils';
import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
-import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue';
+import UrlSyncComponent, {
+ URL_SET_PARAMS_STRATEGY,
+ HISTORY_REPLACE_UPDATE_METHOD,
+} from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
@@ -10,6 +13,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
jest.mock('~/lib/utils/common_utils', () => ({
historyPushState: jest.fn(),
+ historyReplaceState: jest.fn(),
}));
describe('url sync component', () => {
@@ -18,14 +22,12 @@ describe('url sync component', () => {
const findButton = () => wrapper.find('button');
- const createComponent = ({
- query = mockQuery,
- scopedSlots,
- slots,
- urlParamsUpdateStrategy,
- } = {}) => {
+ const createComponent = ({ props = {}, scopedSlots, slots } = {}) => {
wrapper = shallowMount(UrlSyncComponent, {
- propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) },
+ propsData: {
+ query: mockQuery,
+ ...props,
+ },
scopedSlots,
slots,
});
@@ -35,32 +37,27 @@ describe('url sync component', () => {
wrapper.destroy();
});
- const expectUrlSyncFactory = (
+ const expectUrlSyncWithMergeUrlParams = (
query,
times,
- urlParamsUpdateStrategy,
- urlOptions,
- urlReturnValue,
+ mergeUrlParamsReturnValue,
+ historyMethod = historyPushState,
) => {
- expect(urlParamsUpdateStrategy).toHaveBeenCalledTimes(times);
- expect(urlParamsUpdateStrategy).toHaveBeenCalledWith(query, window.location.href, urlOptions);
-
- expect(historyPushState).toHaveBeenCalledTimes(times);
- expect(historyPushState).toHaveBeenCalledWith(urlReturnValue);
- };
+ expect(mergeUrlParams).toHaveBeenCalledTimes(times);
+ expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
+ spreadArrays: true,
+ });
- const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => {
- expectUrlSyncFactory(
- query,
- times,
- mergeUrlParams,
- { spreadArrays: true },
- mergeUrlParamsReturnValue,
- );
+ expect(historyMethod).toHaveBeenCalledTimes(times);
+ expect(historyMethod).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
};
const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => {
- expectUrlSyncFactory(query, times, setUrlParams, true, setUrlParamsReturnValue);
+ expect(setUrlParams).toHaveBeenCalledTimes(times);
+ expect(setUrlParams).toHaveBeenCalledWith(query, window.location.href, true, true, true);
+
+ expect(historyPushState).toHaveBeenCalledTimes(times);
+ expect(historyPushState).toHaveBeenCalledWith(setUrlParamsReturnValue);
};
describe('with query as a props', () => {
@@ -86,13 +83,32 @@ describe('url sync component', () => {
describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => {
it('uses setUrlParams to generate URL', () => {
createComponent({
- urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ props: {
+ urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ },
});
expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value);
});
});
+ describe('with history-update-method equals to HISTORY_REPLACE_UPDATE_METHOD', () => {
+ it('uses historyReplaceState to update the URL', () => {
+ createComponent({
+ props: {
+ historyUpdateMethod: HISTORY_REPLACE_UPDATE_METHOD,
+ },
+ });
+
+ expectUrlSyncWithMergeUrlParams(
+ mockQuery,
+ 1,
+ mergeUrlParams.mock.results[0].value,
+ historyReplaceState,
+ );
+ });
+ });
+
describe('with scoped slot', () => {
const scopedSlots = {
default: `
@@ -101,13 +117,13 @@ describe('url sync component', () => {
};
it('renders the scoped slot', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
expect(findButton().exists()).toBe(true);
});
it('syncs the url with the scoped slots function', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
findButton().trigger('click');
@@ -121,7 +137,7 @@ describe('url sync component', () => {
};
it('renders the default slot', () => {
- createComponent({ query: null, slots });
+ createComponent({ props: { query: null }, slots });
expect(findButton().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 1ad6d043399..63371b1492b 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const TEST_IMAGE_SIZE = 7;
const TEST_BREAKPOINT = 5;
@@ -16,10 +17,13 @@ const createUser = (id) => ({
web_url: `${TEST_HOST}/${id}`,
avatar_url: `${TEST_HOST}/${id}/avatar`,
});
+
const createList = (n) =>
Array(n)
.fill(1)
.map((x, id) => createUser(id));
+const createListCamelCase = (n) =>
+ createList(n).map((user) => convertObjectPropsToCamelCase(user, { deep: true }));
describe('UserAvatarList', () => {
let props;
@@ -75,14 +79,14 @@ describe('UserAvatarList', () => {
props.breakpoint = 0;
});
- it('renders avatars', () => {
+ const linkProps = () =>
+ wrapper.findAllComponents(UserAvatarLink).wrappers.map((x) => x.props());
+
+ it('renders avatars when user has snake_case attributes', () => {
const items = createList(20);
factory({ propsData: { items } });
- const links = wrapper.findAllComponents(UserAvatarLink);
- const linkProps = links.wrappers.map((x) => x.props());
-
- expect(linkProps).toEqual(
+ expect(linkProps()).toEqual(
items.map((x) =>
expect.objectContaining({
linkHref: x.web_url,
@@ -94,6 +98,23 @@ describe('UserAvatarList', () => {
),
);
});
+
+ it('renders avatars when user has camelCase attributes', () => {
+ const items = createListCamelCase(20);
+ factory({ propsData: { items } });
+
+ expect(linkProps()).toEqual(
+ items.map((x) =>
+ expect.objectContaining({
+ linkHref: x.webUrl,
+ imgSrc: x.avatarUrl,
+ imgAlt: x.name,
+ tooltipText: x.name,
+ imgSize: TEST_IMAGE_SIZE,
+ }),
+ ),
+ );
+ });
});
describe('with breakpoint and length equal to breakpoint', () => {
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 874796f653a..b0e9584a15b 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -285,6 +285,20 @@ describe('User select dropdown', () => {
expect(wrapper.emitted('input')).toEqual([[[]]]);
});
+ it('hides the dropdown after clicking on `Unassigned`', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
+ await waitForPromises();
+
+ findUnassignLink().trigger('click');
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ });
+
it('emits an empty array after unselecting the only selected assignee', async () => {
createComponent({
props: {
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 2a0d2089fe3..18afe049149 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal, GlPopover } from '@gitlab/ui';
+import { GlButton, GlLink, GlModal, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
@@ -147,6 +147,11 @@ describe('Web IDE link component', () => {
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
+ const findTryItOutLink = () =>
+ wrapper
+ .findAllComponents(GlLink)
+ .filter((link) => link.text().includes('Try it out'))
+ .at(0);
it.each([
{
@@ -516,6 +521,12 @@ describe('Web IDE link component', () => {
expect(dismiss).toHaveBeenCalled();
});
+ it('dismisses the callout when try it now link is clicked', () => {
+ findTryItOutLink().vm.$emit('click');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
it('dismisses the callout when action button is clicked', () => {
findActionsButton().vm.$emit('actionClicked');
diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
index d59cbce6633..a0b1d64b97c 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
import { issuableTypes } from '~/boards/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
@@ -57,7 +58,7 @@ describe('IssuableBlockedIcon', () => {
item = mockBlockedIssue1,
blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
issuableItem = mockIssue,
- issuableType = issuableTypes.issue,
+ issuableType = TYPE_ISSUE,
} = {}) => {
mockApollo = createMockApollo([
[blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy],
@@ -86,7 +87,7 @@ describe('IssuableBlockedIcon', () => {
data = {},
loading = false,
mockIssuable = mockIssue,
- issuableType = issuableTypes.issue,
+ issuableType = TYPE_ISSUE,
} = {}) => {
wrapper = extendedWrapper(
shallowMount(IssuableBlockedIcon, {
@@ -120,9 +121,9 @@ describe('IssuableBlockedIcon', () => {
};
it.each`
- mockIssuable | issuableType | expectedIcon
- ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'}
- ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
+ ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
`(
'should render blocked icon for $issuableType',
({ mockIssuable, issuableType, expectedIcon }) => {
@@ -152,9 +153,9 @@ describe('IssuableBlockedIcon', () => {
describe('on mouseenter on blocked icon', () => {
it.each`
- item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
- ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
- ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
`(
'should query for blocking issuables and render the result for $issuableType',
async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 43ff68e30b5..221da35de3d 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -16,6 +16,7 @@ import {
} from 'jest/vue_shared/security_reports/mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -187,7 +188,7 @@ describe('Security reports app', () => {
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
- mock.onGet(path).replyOnce(200, successResponse);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
createComponentWithFlagEnabled({
propsData: {
@@ -209,7 +210,7 @@ describe('Security reports app', () => {
describe('when successfully loaded', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(200, successResponse);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
createComponentWithFlagEnabled({
propsData: {
@@ -231,7 +232,7 @@ describe('Security reports app', () => {
describe('when an error occurs', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponentWithFlagEnabled({
propsData: {
@@ -253,7 +254,7 @@ describe('Security reports app', () => {
describe('when the comparison endpoint is not provided', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponentWithFlagEnabled();
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
index 46bfd7eceb1..0cab950cb77 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
import createState from '~/vue_shared/security_reports/store/modules/sast/state';
@@ -99,9 +100,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', () => {
@@ -128,7 +129,7 @@ describe('sast report actions', () => {
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
beforeEach(() => {
rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
+ mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
});
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
@@ -157,9 +158,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(404);
+ .replyOnce(HTTP_STATUS_NOT_FOUND);
});
it('should dispatch the `receiveError` action', () => {
@@ -177,9 +178,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(404)
+ .replyOnce(HTTP_STATUS_NOT_FOUND)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', () => {
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
index 4f4f653bb72..7197784c3e8 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
@@ -99,9 +100,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', () => {
@@ -129,7 +130,7 @@ describe('secret detection report actions', () => {
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
beforeEach(() => {
rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
+ mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
});
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
@@ -158,9 +159,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(404);
+ .replyOnce(HTTP_STATUS_NOT_FOUND);
});
it('should dispatch the `receiveDiffError` action', () => {
@@ -178,9 +179,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(404)
+ .replyOnce(HTTP_STATUS_NOT_FOUND)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', () => {
diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
new file mode 100644
index 00000000000..c8750cd58a0
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
@@ -0,0 +1,63 @@
+import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils';
+import {
+ FEEDBACK_TYPE_DISMISSAL,
+ FEEDBACK_TYPE_ISSUE,
+ FEEDBACK_TYPE_MERGE_REQUEST,
+} from '~/vue_shared/security_reports/constants';
+
+describe('security reports store utils', () => {
+ const vulnerability = { uuid: 1 };
+
+ describe('enrichVulnerabilityWithFeedback', () => {
+ const dismissalFeedback = {
+ feedback_type: FEEDBACK_TYPE_DISMISSAL,
+ finding_uuid: vulnerability.uuid,
+ };
+ const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback };
+
+ const issueFeedback = {
+ feedback_type: FEEDBACK_TYPE_ISSUE,
+ issue_iid: 1,
+ finding_uuid: vulnerability.uuid,
+ };
+ const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback };
+ const mrFeedback = {
+ feedback_type: FEEDBACK_TYPE_MERGE_REQUEST,
+ merge_request_iid: 1,
+ finding_uuid: vulnerability.uuid,
+ };
+ const mrVuln = {
+ ...vulnerability,
+ hasMergeRequest: true,
+ merge_request_feedback: mrFeedback,
+ };
+
+ it.each`
+ feedbacks | expected
+ ${[dismissalFeedback]} | ${dismissalVuln}
+ ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability}
+ ${[issueFeedback]} | ${issueVuln}
+ ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability}
+ ${[mrFeedback]} | ${mrVuln}
+ ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }}
+ `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => {
+ const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
+
+ expect(enrichedVulnerability).toEqual(expected);
+ });
+
+ it('matches correct feedback objects to vulnerability', () => {
+ const feedbacks = [
+ dismissalFeedback,
+ issueFeedback,
+ mrFeedback,
+ { ...dismissalFeedback, finding_uuid: 2 },
+ { ...issueFeedback, finding_uuid: 2 },
+ { ...mrFeedback, finding_uuid: 2 },
+ ];
+ const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
+
+ expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln });
+ });
+ });
+});
diff --git a/spec/frontend/webhooks/components/test_dropdown_spec.js b/spec/frontend/webhooks/components/test_dropdown_spec.js
new file mode 100644
index 00000000000..2f62ca13469
--- /dev/null
+++ b/spec/frontend/webhooks/components/test_dropdown_spec.js
@@ -0,0 +1,63 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { getByRole } from '@testing-library/dom';
+import HookTestDropdown from '~/webhooks/components/test_dropdown.vue';
+
+const mockItems = [
+ {
+ text: 'Foo',
+ href: '#foo',
+ },
+];
+
+describe('HookTestDropdown', () => {
+ let wrapper;
+
+ const findDisclosure = () => wrapper.findComponent(GlDisclosureDropdown);
+ const clickItem = (itemText) => {
+ const item = getByRole(wrapper.element, 'button', { name: itemText });
+ item.dispatchEvent(new MouseEvent('click'));
+ };
+
+ const createComponent = (props) => {
+ wrapper = mount(HookTestDropdown, {
+ propsData: {
+ items: mockItems,
+ ...props,
+ },
+ });
+ };
+
+ it('passes the expected props to GlDisclosureDropdown', () => {
+ const size = 'small';
+ createComponent({ size });
+
+ expect(findDisclosure().props()).toMatchObject({
+ items: mockItems.map((item) => ({
+ text: item.text,
+ })),
+ size,
+ });
+ });
+
+ describe('clicking on an item', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('triggers @rails/ujs data-method=post handling', () => {
+ const railsEventPromise = new Promise((resolve) => {
+ document.addEventListener('click', ({ target }) => {
+ expect(target.tagName).toBe('A');
+ expect(target.dataset.method).toBe('post');
+ expect(target.getAttribute('href')).toBe(mockItems[0].href);
+ resolve();
+ });
+ });
+
+ clickItem(mockItems[0].text);
+
+ return railsEventPromise;
+ });
+ });
+});
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index da95b51c0b1..ee15034daff 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -91,6 +91,7 @@ describe('App', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'version-digest');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
+ property: 'navigation_top',
value: 'namespace-840',
});
});
diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js
index c9614c7330b..5f5e4e53be2 100644
--- a/spec/frontend/whats_new/store/actions_spec.js
+++ b/spec/frontend/whats_new/store/actions_spec.js
@@ -3,6 +3,7 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/whats_new/store/actions';
import * as types from '~/whats_new/store/mutation_types';
@@ -33,7 +34,7 @@ describe('whats new actions', () => {
axiosMock = new MockAdapter(axios);
axiosMock
.onGet('/-/whats_new')
- .replyOnce(200, [{ title: 'Whats New Drawer', url: 'www.url.com' }], {
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'Whats New Drawer', url: 'www.url.com' }], {
'x-next-page': '2',
});
@@ -49,7 +50,7 @@ describe('whats new actions', () => {
axiosMock
.onGet('/-/whats_new', { params: { page: undefined, v: undefined } })
- .replyOnce(200, [{ title: 'GitLab Stories' }]);
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
@@ -66,7 +67,7 @@ describe('whats new actions', () => {
axiosMock
.onGet('/-/whats_new', { params: { page: 8, v: 42 } })
- .replyOnce(200, [{ title: 'GitLab Stories' }]);
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
new file mode 100644
index 00000000000..5901642b8a1
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\"></note-header-stub>"`;
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 3e3b8bf65b2..fd5f373d076 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -6,6 +6,7 @@ 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';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -95,7 +96,7 @@ describe('system note component', () => {
it.skip('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 07c00119398..2a65e91a906 100644
--- a/spec/frontend/work_items/components/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -5,21 +5,23 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { updateDraft } from '~/lib/utils/autosave';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
-import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
-import createNoteMutation from '~/work_items/graphql/create_work_item_note.mutation.graphql';
+import { clearDraft } from '~/lib/utils/autosave';
+import { config } from '~/graphql_shared/issuable_client';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
workItemResponseFactory,
workItemQueryResponse,
projectWorkItemResponse,
createWorkItemNoteResponse,
-} from '../mock_data';
+ mockWorkItemNotesResponse,
+} from '../../mock_data';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/lib/utils/autosave');
@@ -35,18 +37,7 @@ describe('WorkItemCommentForm', () => {
const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
let workItemResponseHandler;
- const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
-
- const setText = (newText) => {
- return findMarkdownEditor().vm.$emit('input', newText);
- };
-
- const clickSave = () =>
- wrapper
- .findAllComponents(GlButton)
- .filter((button) => button.text().startsWith('Comment'))
- .at(0)
- .vm.$emit('click', {});
+ const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -56,6 +47,7 @@ describe('WorkItemCommentForm', () => {
fetchByIid = false,
signedIn = true,
isEditing = true,
+ workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -64,21 +56,36 @@ describe('WorkItemCommentForm', () => {
window.gon.current_user_avatar_url = 'avatar.png';
}
- const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemCommentForm, {
- apolloProvider: createMockApollo([
+ const apolloProvider = createMockApollo(
+ [
[workItemQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
[workItemByIidQuery, workItemByIidResponseHandler],
- ]),
+ ],
+ {},
+ { ...config.cacheConfig },
+ );
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesQuery,
+ variables: {
+ id: workItemId,
+ pageSize: 100,
+ },
+ data: mockWorkItemNotesResponse.data,
+ });
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemAddNote, {
+ apolloProvider,
propsData: {
workItemId: id,
fullPath: 'test-project-path',
queryVariables,
fetchByIid,
+ workItemType,
},
stubs: {
- MarkdownField,
WorkItemCommentLocked,
},
});
@@ -99,9 +106,7 @@ describe('WorkItemCommentForm', () => {
signedIn: true,
});
- setText(noteText);
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', noteText);
await waitForPromises();
@@ -109,6 +114,7 @@ describe('WorkItemCommentForm', () => {
input: {
noteableId: workItemId,
body: noteText,
+ discussionId: null,
},
});
});
@@ -117,9 +123,7 @@ describe('WorkItemCommentForm', () => {
await createComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- setText('test');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'test');
await waitForPromises();
@@ -130,6 +134,33 @@ describe('WorkItemCommentForm', () => {
});
});
+ it('emits `replied` event and hides form after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ queryVariables: {
+ id: mockWorkItemNotesResponse.data.workItem.id,
+ },
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(wrapper.emitted('replied')).toEqual([[]]);
+ });
+
+ it('clears a draft after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
+
it('emits error when mutation returns error', async () => {
const error = 'eror';
@@ -138,16 +169,26 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockResolvedValue({
data: {
createNote: {
- note: null,
+ note: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ __typename: 'CreateNotePayload',
errors: [error],
},
},
}),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
await waitForPromises();
@@ -162,24 +203,12 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([[error]]);
});
-
- it('autosaves', async () => {
- await createComponent({
- isEditing: true,
- });
-
- setText('updated');
-
- expect(updateDraft).toHaveBeenCalled();
- });
});
it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
new file mode 100644
index 00000000000..23a9f285804
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -0,0 +1,164 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as autosave from '~/lib/utils/autosave';
+import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+
+const draftComment = 'draft comment';
+
+jest.mock('~/lib/utils/autosave', () => ({
+ updateDraft: jest.fn(),
+ clearDraft: jest.fn(),
+ getDraft: jest.fn().mockReturnValue(draftComment),
+}));
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
+ confirmAction: jest.fn().mockResolvedValue(true),
+}));
+
+describe('Work item comment form component', () => {
+ let wrapper;
+
+ const mockAutosaveKey = 'test-auto-save-key';
+
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
+ const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
+
+ const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ wrapper = shallowMount(WorkItemCommentForm, {
+ propsData: {
+ workItemType: 'Issue',
+ ariaLabel: 'test-aria-label',
+ autosaveKey: mockAutosaveKey,
+ isSubmitting,
+ initialValue,
+ },
+ provide: {
+ fullPath: 'test-project-path',
+ },
+ });
+ };
+
+ it('passes correct markdown preview path to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
+ '/test-project-path/preview_markdown?target_type=Issue',
+ );
+ });
+
+ it('passes correct form field props to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('formFieldProps')).toEqual({
+ 'aria-label': 'test-aria-label',
+ id: 'work-item-add-or-edit-comment',
+ name: 'work-item-add-or-edit-comment',
+ placeholder: 'Write a comment or drag your files here…',
+ });
+ });
+
+ it('passes correct `loading` prop to confirm button', () => {
+ createComponent({ isSubmitting: true });
+
+ expect(findConfirmButton().props('loading')).toBe(true);
+ });
+
+ it('passes a draft from local storage as a value to markdown editor if the draft exists', () => {
+ createComponent({ initialValue: 'parent comment' });
+ expect(findMarkdownEditor().props('value')).toBe(draftComment);
+ });
+
+ it('passes an initialValue prop as a value to markdown editor if storage draft does not exist', () => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
+ createComponent({ initialValue: 'parent comment' });
+
+ expect(findMarkdownEditor().props('value')).toBe('parent comment');
+ });
+
+ it('passes an empty string as a value to markdown editor if storage draft and initialValue are empty', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('value')).toBe('');
+ });
+
+ describe('on markdown editor input', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets correct comment text value', async () => {
+ expect(findMarkdownEditor().props('value')).toBe('');
+
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+ await nextTick();
+
+ expect(findMarkdownEditor().props('value')).toBe('new comment');
+ });
+
+ it('calls `updateDraft` with correct parameters', async () => {
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+
+ expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
+ });
+ });
+
+ describe('on cancel editing', () => {
+ beforeEach(() => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => draftComment);
+ createComponent();
+ findMarkdownEditor().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
+
+ return waitForPromises();
+ });
+
+ it('confirms a user action if comment text is not empty', () => {
+ expect(confirmViaGlModal.confirmAction).toHaveBeenCalled();
+ });
+
+ it('emits `cancelEditing` and clears draft from the local storage', () => {
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+ });
+
+ it('cancels editing on clicking cancel button', async () => {
+ createComponent();
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+
+ it('emits `submitForm` event on confirm button click', () => {
+ createComponent();
+ findConfirmButton().vm.$emit('click');
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing enter with meta key on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing ctrl+enter on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_comment_locked_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
index 58491c4b09c..734b474c8fc 100644
--- a/spec/frontend/work_items/components/work_item_comment_locked_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
@@ -1,6 +1,6 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) =>
shallowMount(WorkItemCommentLocked, {
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
new file mode 100644
index 00000000000..bb65b75c4d8
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -0,0 +1,149 @@
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import {
+ mockWorkItemCommentNote,
+ mockWorkItemNotesResponseWithComments,
+} from 'jest/work_items/mock_data';
+import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+describe('Work Item Discussion', () => {
+ let wrapper;
+ const mockWorkItemId = 'gid://gitlab/WorkItem/625';
+
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
+ const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
+ const findThreadAtIndex = (index) => findAllThreads().at(index);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
+ const findWorkItemNoteReplying = () => wrapper.findComponent(WorkItemNoteReplying);
+
+ const createComponent = ({
+ discussion = [mockWorkItemCommentNote],
+ workItemId = mockWorkItemId,
+ queryVariables = { id: workItemId },
+ fetchByIid = false,
+ fullPath = 'gitlab-org',
+ workItemType = 'Task',
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDiscussion, {
+ propsData: {
+ discussion,
+ workItemId,
+ queryVariables,
+ fetchByIid,
+ fullPath,
+ workItemType,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Should be wrapped inside the timeline entry item', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ });
+
+ it('should have the author avatar of the work item note', () => {
+ expect(findAvatarLink().exists()).toBe(true);
+ expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
+ expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
+ });
+
+ it('should not show the the toggle replies widget wrapper when no replies', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(false);
+ });
+
+ it('should not show the comment form by default', () => {
+ expect(findWorkItemAddNote().exists()).toBe(false);
+ });
+ });
+
+ describe('When the main comments has threads', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ });
+
+ it('should show the toggle replies widget', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ });
+
+ it('the number of threads should be equal to the response length', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAllThreads()).toHaveLength(
+ mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
+ );
+ });
+
+ it('should autofocus when we click expand replies', async () => {
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ expect(findWorkItemAddNote().exists()).toBe(true);
+ expect(findWorkItemAddNote().props('autofocus')).toBe(true);
+ });
+ });
+
+ describe('When replying to any comment', () => {
+ beforeEach(async () => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ await findWorkItemAddNote().vm.$emit('replying', 'reply text');
+ });
+
+ it('should show optimistic behavior when replying', async () => {
+ expect(findAllThreads()).toHaveLength(2);
+ expect(findWorkItemNoteReplying().exists()).toBe(true);
+ });
+
+ it('should be expanded when the reply is successful', async () => {
+ findWorkItemAddNote().vm.$emit('replied');
+ await nextTick();
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
+ });
+ });
+
+ it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => {
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('deleteNote');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
+ });
+
+ it('emits `error` event when child note emits an `error`', () => {
+ const mockErrorText = 'Houston, we have a problem';
+
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('error', mockErrorText);
+
+ expect(wrapper.emitted('error')).toEqual([[mockErrorText]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
new file mode 100644
index 00000000000..d85cd46c1c3
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+
+describe('Work Item Note Actions', () => {
+ let wrapper;
+
+ const findReplyButton = () => wrapper.findComponent(ReplyButton);
+ const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+
+ const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ wrapper = shallowMount(WorkItemNoteActions, {
+ propsData: {
+ showReply,
+ showEdit,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ it('Should show the reply button by default', () => {
+ createComponent();
+ expect(findReplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('When the reply button needs to be hidden', () => {
+ it('Should show the reply button by default', () => {
+ createComponent({ showReply: false });
+ expect(findReplyButton().exists()).toBe(false);
+ });
+ });
+
+ it('shows edit button when `showEdit` prop is true', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('does not show edit button when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('emits `startEditing` event when edit button is clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
new file mode 100644
index 00000000000..225cc3bacaf
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe('Work Item Note Replying', () => {
+ let wrapper;
+ const mockNoteBody = 'replying body';
+
+ const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+
+ const createComponent = ({ body = mockNoteBody } = {}) => {
+ wrapper = shallowMount(WorkItemNoteReplying, {
+ propsData: {
+ body,
+ },
+ });
+
+ window.gon.current_user_id = '1';
+ window.gon.current_user_avatar_url = 'avatar.png';
+ window.gon.current_user_fullname = 'Administrator';
+ window.gon.current_username = 'user';
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note body and header', () => {
+ expect(findTimelineEntry().exists()).toBe(true);
+ expect(findNoteHeader().html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 7257d5c8023..9b87419cee7 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,53 +1,261 @@
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateDraft } from '~/lib/utils/autosave';
+import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
+import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+Vue.use(VueApollo);
+jest.mock('~/lib/utils/autosave');
+
describe('Work Item Note', () => {
let wrapper;
+ const updatedNoteText = '# Some title';
+ const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
+
+ const successHandler = jest.fn().mockResolvedValue({
+ data: {
+ updateNote: {
+ errors: [],
+ note: {
+ ...mockWorkItemCommentNote,
+ body: updatedNoteText,
+ bodyHtml: updatedNoteBody,
+ },
+ },
+ },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
- const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
- const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findNoteActions = () => wrapper.findComponent(NoteActions);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findEditedAt = () => wrapper.findComponent(EditedAt);
- const createComponent = ({ note = mockWorkItemCommentNote } = {}) => {
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
+
+ const createComponent = ({
+ note = mockWorkItemCommentNote,
+ isFirstNote = false,
+ updateNoteMutationHandler = successHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
note,
+ isFirstNote,
+ workItemType: 'Task',
},
+ apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
});
};
- beforeEach(() => {
- createComponent();
- });
+ describe('when editing', () => {
+ beforeEach(() => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ return nextTick();
+ });
- it('Should be wrapped inside the timeline entry item', () => {
- expect(findTimelineEntryItem().exists()).toBe(true);
- });
+ it('should render a comment form', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('should not render note wrapper', () => {
+ expect(findNoteWrapper().exists()).toBe(false);
+ });
+
+ it('updates saved draft with current note text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ mockWorkItemCommentNote.body,
+ );
+ });
- it('should have the author avatar of the work item note', () => {
- expect(findAvatarLink().exists()).toBe(true);
- expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
+ it('passes correct autosave key prop to comment form component', () => {
+ expect(findCommentForm().props('autosaveKey')).toBe(`${mockWorkItemCommentNote.id}-comment`);
+ });
+
+ it('should hide a form and show wrapper when user cancels editing', async () => {
+ findCommentForm().vm.$emit('cancelEditing');
+ await nextTick();
- expect(findAvatar().exists()).toBe(true);
- expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
- expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
+ expect(findCommentForm().exists()).toBe(false);
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
});
- it('has note header', () => {
- expect(findNoteHeader().exists()).toBe(true);
- expect(findNoteHeader().props('author')).toEqual(mockWorkItemCommentNote.author);
- expect(findNoteHeader().props('createdAt')).toBe(mockWorkItemCommentNote.createdAt);
+ describe('when submitting a form to edit a note', () => {
+ it('calls update mutation with correct variables', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemCommentNote.id,
+ body: updatedNoteText,
+ },
+ });
+ });
+
+ it('hides the form after succesful mutation', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ describe('when mutation fails', () => {
+ beforeEach(async () => {
+ createComponent({ updateNoteMutationHandler: errorHandler });
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+ });
+
+ it('opens the form again', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('updates the saved draft with the latest comment text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ updatedNoteText,
+ );
+ });
+
+ it('emits an error', () => {
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
});
- it('has note body', () => {
- expect(findNoteBody().exists()).toBe(true);
- expect(findNoteBody().props('note')).toEqual(mockWorkItemCommentNote);
+ describe('when not editing', () => {
+ it('should not render a comment form', () => {
+ createComponent();
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ it('should render note wrapper', () => {
+ createComponent();
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
+
+ it('renders no "edited at" information by default', () => {
+ createComponent();
+ expect(findEditedAt().exists()).toBe(false);
+ });
+
+ it('renders "edited at" information if the note was edited', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ lastEditedAt: '2023-02-12T07:47:40Z',
+ lastEditedBy: { ...mockWorkItemCommentNote.author, webPath: 'test-path' },
+ },
+ });
+
+ expect(findEditedAt().exists()).toBe(true);
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: '2023-02-12T07:47:40Z',
+ updatedByName: 'Administrator',
+ updatedByPath: 'test-path',
+ });
+ });
+
+ describe('main comment', () => {
+ beforeEach(() => {
+ createComponent({ isFirstNote: true });
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should not have the Avatar link for main thread inside the timeline-entry', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(false);
+ });
+
+ it('should have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(true);
+ });
+ });
+
+ describe('comment threads', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should have the Avatar link for comment threads', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(true);
+ });
+
+ it('should not have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(false);
+ });
+ });
+
+ it('should display a dropdown if user has a permission to delete a note', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('should not display a dropdown if user has no permission to delete a note', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js
new file mode 100644
index 00000000000..a87233300fc
--- /dev/null
+++ b/spec/frontend/work_items/components/widget_wrapper_spec.js
@@ -0,0 +1,46 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
+
+describe('WidgetWrapper component', () => {
+ let wrapper;
+
+ const createComponent = ({ error } = {}) => {
+ wrapper = shallowMountExtended(WidgetWrapper, { propsData: { error } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+ const findWidgetBody = () => wrapper.findByTestId('widget-body');
+
+ it('is expanded by default', () => {
+ createComponent();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findWidgetBody().exists()).toBe(true);
+ });
+
+ it('collapses on click toggle button', async () => {
+ createComponent();
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findWidgetBody().exists()).toBe(false);
+ });
+
+ it('shows alert when list loading fails', () => {
+ const error = 'Some error';
+ createComponent({ error });
+
+ expect(findAlert().text()).toBe(error);
+ });
+
+ it('emits event when dismissing the alert', () => {
+ createComponent({ error: 'error' });
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('dismissAlert')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
new file mode 100644
index 00000000000..fe31c01df36
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -0,0 +1,104 @@
+import { GlAvatarLink, GlSprintf } 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 WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { workItemResponseFactory, mockAssignees } from '../mock_data';
+
+describe('WorkItemCreatedUpdated component', () => {
+ let wrapper;
+ let successHandler;
+ let successByIidHandler;
+
+ Vue.use(VueApollo);
+
+ const findCreatedAt = () => wrapper.find('[data-testid="work-item-created"]');
+ const findUpdatedAt = () => wrapper.find('[data-testid="work-item-updated"]');
+
+ const findCreatedAtText = () => findCreatedAt().text().replace(/\s+/g, ' ');
+
+ const createComponent = async ({
+ workItemId = 'gid://gitlab/WorkItem/1',
+ workItemIid = '1',
+ fetchByIid = false,
+ author = null,
+ updatedAt,
+ } = {}) => {
+ const workItemQueryResponse = workItemResponseFactory({
+ author,
+ updatedAt,
+ });
+ const byIidResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+ };
+
+ successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ successByIidHandler = jest.fn().mockResolvedValue(byIidResponse);
+
+ const handlers = [
+ [workItemQuery, successHandler],
+ [workItemByIidQuery, successByIidHandler],
+ ];
+
+ wrapper = shallowMount(WorkItemCreatedUpdated, {
+ apolloProvider: createMockApollo(handlers),
+ propsData: { workItemId, workItemIid, fetchByIid, fullPath: '/some/project' },
+ stubs: {
+ GlAvatarLink,
+ GlSprintf,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ describe.each([true, false])('fetchByIid is %s', (fetchByIid) => {
+ describe('work item id and iid undefined', () => {
+ beforeEach(async () => {
+ await createComponent({ workItemId: null, workItemIid: null, fetchByIid });
+ });
+
+ it('skips the work item query', () => {
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ it('shows author name and link', async () => {
+ const author = mockAssignees[0];
+
+ await createComponent({ fetchByIid, author });
+
+ expect(findCreatedAtText()).toEqual(`Created by ${author.name}`);
+ });
+
+ it('shows created time when author is null', async () => {
+ await createComponent({ fetchByIid, author: null });
+
+ expect(findCreatedAtText()).toEqual('Created');
+ });
+
+ it('shows updated time', async () => {
+ await createComponent({ fetchByIid });
+
+ expect(findUpdatedAt().exists()).toBe(true);
+ });
+
+ it('does not show updated time for new work items', async () => {
+ await createComponent({ fetchByIid, updatedAt: null });
+
+ expect(findUpdatedAt().exists()).toBe(false);
+ });
+ });
+});
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 05476ef5ca0..a12ec23c15a 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -16,6 +16,7 @@ import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemDescriptionSubscriptionResponse,
@@ -102,6 +103,49 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
+ describe('editing description with workItemsMvc FF enabled', () => {
+ beforeEach(() => {
+ workItemsMvc = true;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownEditor().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ supportsQuickActions: true,
+ renderMarkdownPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
+ describe('editing description with workItemsMvc FF disabled', () => {
+ beforeEach(() => {
+ workItemsMvc = false;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ markdownPreviewPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
describe.each([true, false])(
'editing description with workItemsMvc %workItemsMvcEnabled',
(workItemsMvcEnabled) => {
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 8976cd6e22b..938cf6e6f51 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
@@ -136,10 +136,14 @@ describe('WorkItemDetailModal component', () => {
it('updates the work item when WorkItemDetail emits `update-modal` event', async () => {
createComponent();
- findWorkItemDetail().vm.$emit('update-modal', null, 'updatedId');
+ findWorkItemDetail().vm.$emit('update-modal', undefined, {
+ id: 'updatedId',
+ iid: 'updatedIid',
+ });
await waitForPromises();
expect(findWorkItemDetail().props().workItemId).toEqual('updatedId');
+ expect(findWorkItemDetail().props().workItemIid).toEqual('updatedIid');
});
describe('delete work item', () => {
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 a50a48de921..64a7502671e 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -16,6 +16,7 @@ import { stubComponent } from 'helpers/stub_component';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
@@ -74,6 +75,7 @@ describe('WorkItemDetail component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
+ const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
@@ -92,6 +94,7 @@ describe('WorkItemDetail component', () => {
isModal = false,
updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
+ workItemIid = '1',
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
@@ -112,7 +115,7 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
- propsData: { isModal, workItemId, workItemIid: '1' },
+ propsData: { isModal, workItemId, workItemIid },
data() {
return {
updateInProgress,
@@ -150,9 +153,9 @@ describe('WorkItemDetail component', () => {
setWindowLocation('');
});
- describe('when there is no `workItemId` prop', () => {
+ describe('when there is no `workItemId` and no `workItemIid` prop', () => {
beforeEach(() => {
- createComponent({ workItemId: null });
+ createComponent({ workItemId: null, workItemIid: null });
});
it('skips the work item query', () => {
@@ -656,6 +659,19 @@ describe('WorkItemDetail component', () => {
});
});
+ it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => {
+ setWindowLocation(`?iid_path=true`);
+
+ createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'group/project',
+ iid: '1',
+ });
+ });
+
describe('hierarchy widget', () => {
it('does not render children tree by default', async () => {
createComponent();
@@ -686,7 +702,7 @@ describe('WorkItemDetail component', () => {
});
it('opens the modal with the child when `show-modal` is emitted', async () => {
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
const event = {
@@ -707,6 +723,7 @@ describe('WorkItemDetail component', () => {
createComponent({
isModal: true,
handler,
+ workItemsMvc2Enabled: true,
});
await waitForPromises();
@@ -749,4 +766,11 @@ describe('WorkItemDetail component', () => {
expect(findNotesWidget().exists()).toBe(true);
});
});
+
+ it('renders created/updated', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findCreatedUpdated().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 083bb5bc4a4..0b6ab5c3290 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -85,7 +85,7 @@ describe('WorkItemLabels component', () => {
it('focuses token selector on token selector input event', async () => {
createComponent();
findTokenSelector().vm.$emit('input', [mockLabels[0]]);
- await nextTick();
+ await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
@@ -189,6 +189,23 @@ describe('WorkItemLabels component', () => {
);
});
+ it('adds new labels to the end', async () => {
+ const response = workItemResponseFactory({ labels: [mockLabels[1]] });
+ const workItemQueryHandler = jest.fn().mockResolvedValue(response);
+ createComponent({
+ workItemQueryHandler,
+ updateWorkItemMutationHandler: successUpdateWorkItemMutationHandler,
+ });
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', [mockLabels[0]]);
+ await waitForPromises();
+
+ const labels = findTokenSelector().props('selectedTokens');
+ expect(labels[0]).toMatchObject(mockLabels[1]);
+ expect(labels[1]).toMatchObject(mockLabels[0]);
+ });
+
describe('when clicking outside the token selector', () => {
it('calls a mutation with correct variables', () => {
createComponent();
@@ -205,9 +222,7 @@ describe('WorkItemLabels component', () => {
});
it('emits an error and resets labels if mutation was rejected', async () => {
- const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
-
- createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler });
+ createComponent({ updateWorkItemMutationHandler: errorHandler });
await waitForPromises();
@@ -224,6 +239,23 @@ describe('WorkItemLabels component', () => {
expect(updatedLabels).toEqual(initialLabels);
});
+ it('does not make server request if no labels added or removed', async () => {
+ const updateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
+ createComponent({ updateWorkItemMutationHandler });
+
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', []);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+
it('has a subscription', async () => {
createComponent();
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 5e1c46826cc..480f8fbcc58 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
@@ -40,7 +40,6 @@ describe('WorkItemLinksForm', () => {
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
- workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE,
@@ -62,9 +61,6 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
- },
projectPath: 'project/path',
hasIterationsFeature,
},
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 a61de78c623..ec51f92b578 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,5 +1,4 @@
import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,6 +7,8 @@ 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 { resolvers } from '~/graphql_shared/issuable_client';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
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';
@@ -17,6 +18,7 @@ import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
+ getIssueDetailsResponse,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@@ -27,39 +29,6 @@ import {
Vue.use(VueApollo);
-const issueDetailsResponse = (confidential = false) => ({
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- title: null,
- startDate: '2022-06-22',
- dueDate: '2022-07-19',
- webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
- iterationCadence: {
- id: 'gid://gitlab/Iterations::Cadence/1101',
- title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
- __typename: 'IterationCadence',
- },
- __typename: 'Iteration',
- },
- milestone: {
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/28',
- title: 'v2.0',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
- },
- __typename: 'Project',
- },
- },
-});
const showModal = jest.fn();
describe('WorkItemLinks', () => {
@@ -83,7 +52,7 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
fetchByIid = false,
} = {}) => {
@@ -95,7 +64,7 @@ describe('WorkItemLinks', () => {
[issueDetailsQuery, issueDetailsQueryHandler],
[workItemByIidQuery, childWorkItemByIidHandler],
],
- {},
+ resolvers,
{ addTypename: true },
);
@@ -127,12 +96,12 @@ describe('WorkItemLinks', () => {
},
});
+ wrapper.vm.$refs.wrapper.show = jest.fn();
+
await waitForPromises();
};
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findToggleButton = () => wrapper.findByTestId('toggle-links');
- const findLinksBody = () => wrapper.findByTestId('links-body');
+ const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
@@ -142,31 +111,14 @@ describe('WorkItemLinks', () => {
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
- beforeEach(async () => {
- await createComponent();
- });
-
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
setWindowLocation('');
});
- it('is expanded by default', () => {
- expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
- expect(findLinksBody().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(findLinksBody().exists()).toBe(false);
- });
-
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
@@ -181,6 +133,7 @@ describe('WorkItemLinks', () => {
});
it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleCreateFormButton().vm.$emit('click');
await nextTick();
@@ -193,6 +146,24 @@ describe('WorkItemLinks', () => {
expect(findAddLinksForm().exists()).toBe(false);
});
+
+ it('adds work item child from the form', async () => {
+ const workItem = {
+ ...workItemQueryResponse.data.workItem,
+ id: 'gid://gitlab/WorkItem/11',
+ };
+ await createComponent();
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleCreateFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
+
+ findAddLinksForm().vm.$emit('addWorkItemChild', workItem);
+ await waitForPromises();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(5);
+ });
});
describe('when no child links', () => {
@@ -207,8 +178,8 @@ describe('WorkItemLinks', () => {
});
});
- it('renders all hierarchy widget children', () => {
- expect(findLinksBody().exists()).toBe(true);
+ it('renders all hierarchy widget children', async () => {
+ await createComponent();
expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
@@ -219,15 +190,13 @@ describe('WorkItemLinks', () => {
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
});
- await nextTick();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorMessage);
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
- it('displays number if children', () => {
- expect(findChildrenCount().exists()).toBe(true);
+ it('displays number of children', async () => {
+ await createComponent();
+ expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
});
@@ -294,7 +263,9 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ issueDetailsQueryHandler: jest
+ .fn()
+ .mockResolvedValue(getIssueDetailsResponse({ confidential: true })),
});
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index 156f06a0d5e..0236fe2e60d 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -23,8 +23,6 @@ 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);
@@ -64,36 +62,25 @@ describe('WorkItemTree', () => {
projectPath: 'test/project',
},
});
+
+ wrapper.vm.$refs.wrapper.show = jest.fn();
};
- beforeEach(() => {
+ it('displays Add button', () => {
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', () => {
+ createComponent();
+
const workItemLinkChildren = findWorkItemLinkChildItems();
expect(workItemLinkChildren).toHaveLength(4);
expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
@@ -102,6 +89,8 @@ describe('WorkItemTree', () => {
});
it('does not display form by default', () => {
+ createComponent();
+
expect(findForm().exists()).toBe(false);
});
@@ -114,6 +103,8 @@ describe('WorkItemTree', () => {
`(
'when selecting $option from split button, renders the form passing $formType and $childType',
async ({ event, formType, childType }) => {
+ createComponent();
+
findToggleFormSplitButton().vm.$emit(event);
await nextTick();
@@ -128,13 +119,16 @@ describe('WorkItemTree', () => {
);
it('remove event on child triggers `removeChild` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
+
firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
});
it('emits `show-modal` on `click` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
const event = {
childItem: 'gid://gitlab/WorkItem/2',
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 23dd2b6bacb..3db848a0ad2 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -1,22 +1,26 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
-import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
+import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
import ActivityFilter from '~/work_items/components/notes/activity_filter.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 workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
-import { DESC } from '~/notes/constants';
+import { ASC, DESC } from '~/notes/constants';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
mockWorkItemNotesByIidResponse,
mockMoreWorkItemNotesResponse,
+ mockWorkItemNotesResponseWithComments,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
@@ -32,34 +36,56 @@ const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id;
+const mockDiscussions = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes;
+
describe('WorkItemNotes component', () => {
let wrapper;
Vue.use(VueApollo);
+ const showModal = jest.fn();
+
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
+ const findAllListItems = () => wrapper.findAll('ul.timeline > *');
const findActivityLabel = () => wrapper.find('label');
- const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
+ const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
+ const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
+ const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
+
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
const workItemNotesByIidQueryHandler = jest
.fn()
.mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
+ const workItemNotesWithCommentsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments);
+ const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
workItemId = mockWorkItemId,
fetchByIid = false,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
+ deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
[workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ [deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
]),
propsData: {
workItemId,
@@ -75,6 +101,9 @@ describe('WorkItemNotes component', () => {
useIidInWorkItemsPath: fetchByIid,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
+ },
});
};
@@ -87,10 +116,14 @@ describe('WorkItemNotes component', () => {
});
it('passes correct props to comment form component', async () => {
- createComponent({ workItemId: mockWorkItemId, fetchByIid: false });
+ createComponent({
+ workItemId: mockWorkItemId,
+ fetchByIid: false,
+ defaultWorkItemNotesQueryHandler: workItemNotesByIidQueryHandler,
+ });
await waitForPromises();
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false);
});
describe('when notes are loading', () => {
@@ -121,13 +154,14 @@ describe('WorkItemNotes component', () => {
});
it('renders the notes list to the length of the response', () => {
+ expect(workItemNotesByIidQueryHandler).toHaveBeenCalled();
expect(findAllSystemNotes()).toHaveLength(
mockNotesByIidWidgetResponse.discussions.nodes.length,
);
});
it('passes correct props to comment form component', () => {
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true);
});
});
@@ -180,5 +214,124 @@ describe('WorkItemNotes component', () => {
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
+
+ it('puts form at start of list in when sorting by newest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+
+ expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
+ });
+
+ it('puts form at end of list in when sorting by oldest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+
+ expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
+ });
+ });
+
+ describe('Activity comments', () => {
+ beforeEach(async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ });
+
+ it('should not have any system notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllSystemNotes()).toHaveLength(0);
+ });
+
+ it('should have work item notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(mockDiscussions.length);
+ });
+
+ it('should pass all the correct props to work item comment note', () => {
+ const commentIndex = 0;
+ const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
+
+ expect(firstCommentNote.props('discussion')).toEqual(
+ mockDiscussions[commentIndex].notes.nodes,
+ );
+ });
+ });
+
+ it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
+ expect(showModal).toHaveBeenCalled();
+ });
+
+ describe('when modal is open', () => {
+ beforeEach(() => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ return waitForPromises();
+ });
+
+ it('sends the mutation with correct variables', () => {
+ const noteId = 'some-test-id';
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: noteId,
+ },
+ });
+ });
+
+ it('successfully removes the note from the discussion', async () => {
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2);
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1);
+ });
+
+ it('successfully removes the discussion from work item if discussion only had one note', async () => {
+ const secondDiscussion = findWorkItemCommentNoteAtIndex(1);
+
+ expect(findAllWorkItemCommentNotes()).toHaveLength(2);
+ expect(secondDiscussion.props('discussion')).toHaveLength(1);
+
+ secondDiscussion.vm.$emit('deleteNote', {
+ id: mockDiscussions[1].notes.nodes[0].id,
+ discussion: { id: mockDiscussions[1].id },
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(1);
+ });
+ });
+
+ it('emits `error` event if delete note mutation is rejected', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ deleteWINoteMutationHandler: errorHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong when deleting a comment. Please try again'],
+ ]);
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 67b477b6eb0..d4832fe376d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -57,7 +57,16 @@ export const workItemQueryResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ avatarUrl: 'http://127.0.0.1:3000/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',
+ },
project: {
__typename: 'Project',
id: '1',
@@ -113,6 +122,7 @@ export const workItemQueryResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -152,7 +162,11 @@ export const updateWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: '2022-08-08T12:41:54Z',
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -176,6 +190,7 @@ export const updateWorkItemMutationResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -200,6 +215,14 @@ export const updateWorkItemMutationResponse = {
nodes: [mockAssignees[0]],
},
},
+ {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels: false,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
],
},
},
@@ -264,7 +287,6 @@ export const workItemResponseFactory = ({
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
- labelsWidgetPresent = true,
weightWidgetPresent = true,
progressWidgetPresent = true,
milestoneWidgetPresent = true,
@@ -273,12 +295,17 @@ export const workItemResponseFactory = ({
notesWidgetPresent = true,
confidential = false,
canInviteMembers = false,
+ labelsWidgetPresent = true,
+ labels = mockLabels,
allowsScopedLabels = false,
lastEditedAt = null,
lastEditedBy = null,
withCheckboxes = false,
parent = mockParent.parent,
workItemType = taskType,
+ author = mockAssignees[0],
+ createdAt = '2022-08-03T12:41:54Z',
+ updatedAt = '2022-08-08T12:32:54Z',
} = {}) => ({
data: {
workItem: {
@@ -289,8 +316,10 @@ export const workItemResponseFactory = ({
state: 'OPEN',
description: 'description',
confidential,
- createdAt: '2022-08-03T12:41:54Z',
+ createdAt,
+ updatedAt,
closedAt: null,
+ author,
project: {
__typename: 'Project',
id: '1',
@@ -330,7 +359,7 @@ export const workItemResponseFactory = ({
type: 'LABELS',
allowsScopedLabels,
labels: {
- nodes: mockLabels,
+ nodes: labels,
},
}
: { type: 'MOCK TYPE' },
@@ -409,6 +438,7 @@ export const workItemResponseFactory = ({
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '5',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -441,6 +471,28 @@ export const workItemResponseFactory = ({
},
});
+export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ issuable: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
+ },
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -470,7 +522,11 @@ export const createWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -494,6 +550,16 @@ export const createWorkItemMutationResponse = {
},
};
+export const createWorkItemMutationErrorResponse = {
+ data: {
+ workItemCreate: {
+ __typename: 'WorkItemCreatePayload',
+ workItem: null,
+ errors: ['an error'],
+ },
+ },
+};
+
export const createWorkItemFromTaskMutationResponse = {
data: {
workItemCreateFromTask: {
@@ -1045,11 +1111,15 @@ export const workItemObjectiveWithChild = {
deleteWorkItem: true,
updateWorkItem: true,
},
+ author: {
+ ...mockAssignees[0],
+ },
title: 'Objective',
description: 'Objective description',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
widgets: [
{
@@ -1190,7 +1260,11 @@ export const changeWorkItemParentMutationResponse = {
title: 'Foo',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -1557,7 +1631,7 @@ export const projectWorkItemResponse = {
export const mockWorkItemNotesResponse = {
data: {
workItem: {
- id: 'gid://gitlab/WorkItem/600',
+ id: 'gid://gitlab/WorkItem/1',
iid: '60',
widgets: [
{
@@ -1596,20 +1670,30 @@ export const mockWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1629,20 +1713,30 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1662,19 +1756,29 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1753,20 +1857,31 @@ export const mockWorkItemNotesByIidResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
nodes: [
{
id: 'gid://gitlab/Note/2428',
+ body: 'added 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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1786,21 +1901,32 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id:
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1820,21 +1946,33 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
+ id: 'gid://gitlab/Discussion/addbc177f7664699a135130ab05ffb78c57e4db3',
notes: {
nodes: [
{
id:
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
+ body:
+ 'changed iteration to Et autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1910,20 +2048,30 @@ export const mockMoreWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1943,20 +2091,30 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1976,19 +2134,29 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2022,6 +2190,55 @@ export const createWorkItemNoteResponse = {
data: {
createNote: {
errors: [],
+ note: {
+ id: 'gid://gitlab/Note/569',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/569',
+ body: 'Main comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Main comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-25T04:49:46Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ body: 'Latest 22',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Latest 22</p>',
+ __typename: 'Note',
+ },
__typename: 'CreateNotePayload',
},
},
@@ -2029,14 +2246,25 @@ export const createWorkItemNoteResponse = {
export const mockWorkItemCommentNote = {
id: 'gid://gitlab/Note/158',
+ body: 'How are you ? what do you think about this ?',
bodyHtml:
'<p data-sourcepos="1:1-1:76" dir="auto"><gl-emoji title="waving hand sign" data-name="wave" data-unicode-version="6.0">👋</gl-emoji> Hi <a href="/fredda.brekke" data-reference-type="user" data-user="3" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Sherie Nitzsche">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji title="person with folded hands" data-name="pray" data-unicode-version="6.0">🙏</gl-emoji></p>',
systemNoteIconName: false,
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: false,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2048,3 +2276,174 @@ export const mockWorkItemCommentNote = {
__typename: 'UserCore',
},
};
+
+export const mockWorkItemNotesResponseWithComments = {
+ 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/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/174',
+ body: 'Separate thread',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-12T07:47:40Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/235',
+ body: 'Thread comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-18T09:09:54Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'Main thread 2',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ system: false,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ 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',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
new file mode 100644
index 00000000000..aa24b80cf08
--- /dev/null
+++ b/spec/frontend/work_items/utils_spec.js
@@ -0,0 +1,27 @@
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+
+describe('autocompleteDataSources', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(autocompleteDataSources('project/group', '2')).toMatchObject({
+ commands: '/foobar/project/group/-/autocomplete_sources/commands?type=WorkItem&type_id=2',
+ labels: '/foobar/project/group/-/autocomplete_sources/labels?type=WorkItem&type_id=2',
+ members: '/foobar/project/group/-/autocomplete_sources/members?type=WorkItem&type_id=2',
+ });
+ });
+});
+
+describe('markdownPreviewPath', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(markdownPreviewPath('project/group', '2')).toEqual(
+ '/foobar/project/group/preview_markdown?target_type=WorkItem&target_id=2',
+ );
+ });
+});
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index a88910b2613..85f1dbdc305 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -6,6 +6,7 @@ import Mousetrap from 'mousetrap';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GLForm from '~/gl_form';
import * as utils from '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ZenMode from '~/zen_mode';
describe('ZenMode', () => {
@@ -32,7 +33,7 @@ describe('ZenMode', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
loadHTMLFixture(fixtureName);
diff --git a/spec/frontend_integration/ide/helpers/mock_data.js b/spec/frontend_integration/ide/helpers/mock_data.js
index 8c9ec74541f..e012507a592 100644
--- a/spec/frontend_integration/ide/helpers/mock_data.js
+++ b/spec/frontend_integration/ide/helpers/mock_data.js
@@ -5,7 +5,5 @@ export const IDE_DATASET = {
pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
promotionSvgPath: '/test/promotion.svg',
webIDEHelpPagePath: '/test/web_ide_help_page',
- clientsidePreviewEnabled: 'true',
renderWhitespaceInCode: 'false',
- codesandboxBundlerUrl: 'test/codesandbox_bundler',
};
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/404.js b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
index 54183f1189c..38742087675 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/404.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/404.js
@@ -1,9 +1,10 @@
import { Response } from 'miragejs';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
export default (server) => {
['get', 'post', 'put', 'delete', 'patch'].forEach((method) => {
server[method]('*', () => {
- return new Response(404);
+ return new Response(HTTP_STATUS_NOT_FOUND);
});
});
};
diff --git a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
index 64e9006a710..83991ad5af9 100644
--- a/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
+++ b/spec/frontend_integration/test_helpers/mock_server/routes/emojis.js
@@ -1,9 +1,10 @@
import { Response } from 'miragejs';
import emojis from 'public/-/emojis/2/emojis.json';
import { EMOJI_VERSION } from '~/emoji';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
export default (server) => {
server.get(`/-/emojis/${EMOJI_VERSION}/emojis.json`, () => {
- return new Response(200, {}, emojis);
+ return new Response(HTTP_STATUS_OK, {}, emojis);
});
};
diff --git a/spec/graphql/mutations/achievements/create_spec.rb b/spec/graphql/mutations/achievements/create_spec.rb
index 4bad6164314..12b8ff67549 100644
--- a/spec/graphql/mutations/achievements/create_spec.rb
+++ b/spec/graphql/mutations/achievements/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Achievements::Create, feature_category: :users do
+RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 727db7e2361..44147987ebb 100644
--- a/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Mutations::Ci::JobTokenScope::AddProject do
+RSpec.describe Mutations::Ci::JobTokenScope::AddProject, feature_category: :continuous_integration do
let(:mutation) do
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
end
@@ -14,9 +14,10 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject do
let_it_be(:target_project) { create(:project) }
let(:target_project_path) { target_project.full_path }
+ let(:mutation_args) { { project_path: project.full_path, target_project_path: target_project_path } }
subject do
- mutation.resolve(project_path: project.full_path, target_project_path: target_project_path)
+ mutation.resolve(**mutation_args)
end
context 'when user is not logged in' do
@@ -42,18 +43,45 @@ RSpec.describe Mutations::Ci::JobTokenScope::AddProject do
target_project.add_guest(current_user)
end
- it 'adds target project to the job token scope' do
+ it 'adds target project to the outbound job token scope by default' do
expect do
expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
- end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end
+
+ context 'when mutation uses the direction argument' do
+ let(:mutation_args) { super().merge!(direction: direction) }
+
+ context 'when targeting the outbound allowlist' do
+ let(:direction) { :outbound }
+
+ it 'adds the target project' do
+ expect do
+ expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end
+ end
+
+ context 'when targeting the inbound allowlist' do
+ let(:direction) { :inbound }
+
+ it 'adds the target project' do
+ expect do
+ expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
+ end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1)
+ end
+ end
end
context 'when the service returns an error' do
let(:service) { double(:service) }
it 'returns an error response' do
- expect(::Ci::JobTokenScope::AddProjectService).to receive(:new).with(project, current_user).and_return(service)
- expect(service).to receive(:execute).with(target_project).and_return(ServiceResponse.error(message: 'The error message'))
+ expect(::Ci::JobTokenScope::AddProjectService).to receive(:new).with(
+ project,
+ current_user
+ ).and_return(service)
+ expect(service).to receive(:execute).with(target_project, direction: :outbound).and_return(ServiceResponse.error(message: 'The error message'))
expect(subject.fetch(:ci_job_token_scope)).to be_nil
expect(subject.fetch(:errors)).to include("The error message")
diff --git a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index d399e73f394..5385b6ca1cf 100644
--- a/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject do
+RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject, feature_category: :continuous_integration do
let(:mutation) do
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
end
@@ -17,6 +17,7 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject do
end
let(:target_project_path) { target_project.full_path }
+ let(:links_relation) { Ci::JobToken::ProjectScopeLink.with_source(project).with_target(target_project) }
subject do
mutation.resolve(project_path: project.full_path, target_project_path: target_project_path)
@@ -45,18 +46,40 @@ RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject do
target_project.add_guest(current_user)
end
- it 'removes target project from the job token scope' do
- expect do
- expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
- end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
+ let(:service) { instance_double('Ci::JobTokenScope::RemoveProjectService') }
+
+ context 'with no direction specified' do
+ it 'defaults to asking the RemoveProjectService to remove the outbound link' do
+ expect(::Ci::JobTokenScope::RemoveProjectService)
+ .to receive(:new).with(project, current_user).and_return(service)
+ expect(service).to receive(:execute).with(target_project, :outbound)
+ .and_return(instance_double('ServiceResponse', "success?": true))
+
+ subject
+ end
+ end
+
+ context 'with direction specified' do
+ subject do
+ mutation.resolve(project_path: project.full_path, target_project_path: target_project_path, direction: 'inbound')
+ end
+
+ it 'executes project removal for the correct direction' do
+ expect(::Ci::JobTokenScope::RemoveProjectService)
+ .to receive(:new).with(project, current_user).and_return(service)
+ expect(service).to receive(:execute).with(target_project, 'inbound')
+ .and_return(instance_double('ServiceResponse', "success?": true))
+
+ subject
+ end
end
context 'when the service returns an error' do
- let(:service) { double(:service) }
+ let(:service) { instance_double('Ci::JobTokenScope::RemoveProjectService') }
it 'returns an error response' do
expect(::Ci::JobTokenScope::RemoveProjectService).to receive(:new).with(project, current_user).and_return(service)
- expect(service).to receive(:execute).with(target_project).and_return(ServiceResponse.error(message: 'The error message'))
+ expect(service).to receive(:execute).with(target_project, :outbound).and_return(ServiceResponse.error(message: 'The error message'))
expect(subject.fetch(:ci_job_token_scope)).to be_nil
expect(subject.fetch(:errors)).to include("The error message")
diff --git a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
new file mode 100644
index 00000000000..564bc95b352
--- /dev/null
+++ b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do
+ specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') }
+
+ it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) }
+end
diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb
index bb57ad4c404..324f225f209 100644
--- a/spec/graphql/mutations/issues/update_spec.rb
+++ b/spec/graphql/mutations/issues/update_spec.rb
@@ -46,10 +46,12 @@ RSpec.describe Mutations::Issues::Update do
project.add_developer(user)
end
- it 'updates issue with correct values' do
- subject
+ context 'when all attributes except timeEstimate are provided' do
+ it 'updates issue with correct values' do
+ subject
- expect(issue.reload).to have_attributes(expected_attributes)
+ expect(issue.reload).to have_attributes(expected_attributes)
+ end
end
context 'when iid does not exist' do
@@ -162,6 +164,39 @@ RSpec.describe Mutations::Issues::Update do
expect { subject }.to change { issue.reload.issue_type }.from('issue').to('incident')
end
end
+
+ context 'when timeEstimate attribute is provided' do
+ let_it_be_with_refind(:issue) { create(:issue, project: project, time_estimate: 3600) }
+
+ let(:time_estimate) { '0' }
+ let(:expected_attributes) { { time_estimate: time_estimate } }
+
+ context 'when timeEstimate is invalid' do
+ let(:time_estimate) { '1e' }
+
+ it 'raises an argument error and changes are not applied' do
+ expect { mutation.ready?(time_estimate: time_estimate) }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'timeEstimate must be formatted correctly, for example `1h 30m`')
+ expect { subject }.not_to change { issue.reload.time_estimate }
+ end
+ end
+
+ context 'when timeEstimate is 0' do
+ let(:time_estimate) { '0' }
+
+ it 'resets the time estimate' do
+ expect { subject }.to change { issue.reload.time_estimate }.from(3600).to(0)
+ end
+ end
+
+ context 'when timeEstimate is a valid human readable time' do
+ let(:time_estimate) { '1h 30m' }
+
+ it 'updates the time estimate' do
+ expect { subject }.to change { issue.reload.time_estimate }.from(3600).to(5400)
+ end
+ end
+ end
end
end
end
diff --git a/spec/graphql/mutations/merge_requests/update_spec.rb b/spec/graphql/mutations/merge_requests/update_spec.rb
index 206abaf34ce..8a10f6cadd0 100644
--- a/spec/graphql/mutations/merge_requests/update_spec.rb
+++ b/spec/graphql/mutations/merge_requests/update_spec.rb
@@ -26,10 +26,56 @@ RSpec.describe Mutations::MergeRequests::Update do
merge_request.project.add_developer(user)
end
- it 'applies all attributes' do
- expect(mutated_merge_request).to eq(merge_request)
- expect(mutated_merge_request).to have_attributes(attributes)
- expect(subject[:errors]).to be_empty
+ context 'when all attributes except timeEstimate are provided' do
+ before do
+ merge_request.update!(time_estimate: 3600)
+ end
+
+ it 'applies all attributes' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request).to have_attributes(attributes)
+ expect(mutated_merge_request.time_estimate).to eq(3600)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when timeEstimate attribute is provided' do
+ let(:time_estimate) { '0' }
+ let(:attributes) { { time_estimate: time_estimate } }
+
+ before do
+ merge_request.update!(time_estimate: 3600)
+ end
+
+ context 'when timeEstimate is invalid' do
+ let(:time_estimate) { '1e' }
+
+ it 'changes are not applied' do
+ expect { mutation.ready?(time_estimate: time_estimate) }
+ .to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ 'timeEstimate must be formatted correctly, for example `1h 30m`')
+ expect(mutated_merge_request.time_estimate).to eq(3600)
+ end
+ end
+
+ context 'when timeEstimate is 0' do
+ let(:time_estimate) { '0' }
+
+ it 'resets the time estimate' do
+ expect(mutated_merge_request.time_estimate).to eq(0)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when timeEstimate is a valid human readable time' do
+ let(:time_estimate) { '1h 30m' }
+
+ it 'updates the time estimate' do
+ expect(mutated_merge_request.time_estimate).to eq(5400)
+ expect(subject[:errors]).to be_empty
+ end
+ end
end
context 'the merge request is invalid' do
@@ -82,4 +128,53 @@ RSpec.describe Mutations::MergeRequests::Update do
end
end
end
+
+ describe '#ready?' do
+ let(:extra_args) { {} }
+
+ let(:arguments) do
+ {
+ project_path: merge_request.project.full_path,
+ iid: merge_request.iid
+ }.merge(extra_args)
+ end
+
+ subject(:ready) { mutation.ready?(**arguments) }
+
+ context 'when required arguments are not provided' do
+ let(:arguments) { {} }
+
+ it 'raises an argument error' do
+ expect { subject }.to raise_error(ArgumentError, 'Arguments must be provided: projectPath, iid')
+ end
+ end
+
+ context 'when required arguments are provided' do
+ it 'returns true' do
+ expect(subject).to eq(true)
+ end
+ end
+
+ context 'when timeEstimate is provided' do
+ let(:extra_args) { { time_estimate: time_estimate } }
+
+ context 'when the value is invalid' do
+ let(:time_estimate) { '1e' }
+
+ it 'raises an argument error' do
+ expect { subject }.to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ 'timeEstimate must be formatted correctly, for example `1h 30m`')
+ end
+ end
+
+ context 'when the value valid' do
+ let(:time_estimate) { '1d' }
+
+ it 'returns true' do
+ expect(subject).to eq(true)
+ end
+ end
+ end
+ end
end
diff --git a/spec/graphql/mutations/saved_replies/create_spec.rb b/spec/graphql/mutations/saved_replies/create_spec.rb
index 5141c537b06..9423ba2b354 100644
--- a/spec/graphql/mutations/saved_replies/create_spec.rb
+++ b/spec/graphql/mutations/saved_replies/create_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Mutations::SavedReplies::Create do
let(:mutation_arguments) { { name: '', content: '' } }
it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank", "Name can contain only lowercase letters, digits, '_' and '-'. Must start with a letter, and cannot end with '-' or '_'"]) }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
end
context 'when service successfully creates a new saved reply' do
diff --git a/spec/graphql/mutations/saved_replies/update_spec.rb b/spec/graphql/mutations/saved_replies/update_spec.rb
index 67c2d1348f7..9b0e90b7b41 100644
--- a/spec/graphql/mutations/saved_replies/update_spec.rb
+++ b/spec/graphql/mutations/saved_replies/update_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Mutations::SavedReplies::Update do
let(:mutation_arguments) { { name: '', content: '' } }
it { expect(subject[:saved_reply]).to be_nil }
- it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank", "Name can contain only lowercase letters, digits, '_' and '-'. Must start with a letter, and cannot end with '-' or '_'"]) }
+ it { expect(subject[:errors]).to match_array(["Content can't be blank", "Name can't be blank"]) }
end
context 'when service successfully updates the saved reply' do
diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
index 59ece15b745..92f4d3dd8e8 100644
--- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
@@ -23,18 +23,18 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
it 'returns the same project in the allow list of projects for the Ci Job Token when scope is not enabled' do
allow(project).to receive(:ci_outbound_job_token_scope_enabled?).and_return(false)
- expect(resolve_scope.all_projects).to contain_exactly(project)
+ expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
it 'returns the same project in the allow list of projects for the Ci Job Token' do
- expect(resolve_scope.all_projects).to contain_exactly(project)
+ expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
context 'when another projects gets added to the allow list' do
let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) }
it 'returns both projects' do
- expect(resolve_scope.all_projects).to contain_exactly(project, link.target_project)
+ expect(resolve_scope.outbound_projects).to contain_exactly(project, link.target_project)
end
end
@@ -44,7 +44,7 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
end
it 'resolves projects' do
- expect(resolve_scope.all_projects).to contain_exactly(project)
+ expect(resolve_scope.outbound_projects).to contain_exactly(project)
end
end
end
diff --git a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
index 1d1fb4a9967..da6a84cec44 100644
--- a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Resolvers::Ci::RunnerPlatformsResolver, feature_category: :runner
expect(resolve_subject).to contain_exactly(
hash_including(name: :linux), hash_including(name: :osx),
hash_including(name: :windows), hash_including(name: :docker),
- hash_including(name: :kubernetes)
+ hash_including(name: :kubernetes), hash_including(name: :aws)
)
end
end
diff --git a/spec/graphql/resolvers/ci/variables_resolver_spec.rb b/spec/graphql/resolvers/ci/variables_resolver_spec.rb
new file mode 100644
index 00000000000..16b72e8cb7f
--- /dev/null
+++ b/spec/graphql/resolvers/ci/variables_resolver_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::VariablesResolver, feature_category: :pipeline_authoring do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:args) { {} }
+ let_it_be(:obj) { nil }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
+
+ let_it_be(:ci_instance_variables) do
+ [
+ create(:ci_instance_variable, key: 'a'),
+ create(:ci_instance_variable, key: 'b')
+ ]
+ end
+
+ let_it_be(:ci_group_variables) do
+ [
+ create(:ci_group_variable, group: group, key: 'a'),
+ create(:ci_group_variable, group: group, key: 'b')
+ ]
+ end
+
+ let_it_be(:ci_project_variables) do
+ [
+ create(:ci_variable, project: project, key: 'a'),
+ create(:ci_variable, project: project, key: 'b')
+ ]
+ end
+
+ subject(:resolve_variables) { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) }
+
+ context 'when parent object is nil' do
+ context 'when user is authorized', :enable_admin_mode do
+ let_it_be(:user) { create(:admin) }
+
+ it "returns the instance's variables" do
+ expect(resolve_variables.items.to_a).to match_array(ci_instance_variables)
+ end
+ end
+
+ context 'when user is not authorized' do
+ it "returns nil" do
+ expect(resolve_variables).to be_nil
+ end
+ end
+ end
+
+ context 'when parent object is a Group' do
+ let_it_be(:obj) { group }
+
+ it "returns the group's variables" do
+ expect(resolve_variables.items.to_a).to match_array(ci_group_variables)
+ end
+ end
+
+ context 'when parent object is a Project' do
+ let_it_be(:obj) { project }
+
+ it "returns the project's variables" do
+ expect(resolve_variables.items.to_a).to match_array(ci_project_variables)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/data_transfer_resolver_spec.rb b/spec/graphql/resolvers/data_transfer_resolver_spec.rb
new file mode 100644
index 00000000000..f5a088dc1c3
--- /dev/null
+++ b/spec/graphql/resolvers/data_transfer_resolver_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DataTransferResolver, feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ describe '.source' do
+ context 'with base DataTransferResolver' do
+ it 'raises NotImplementedError' do
+ expect { described_class.source }.to raise_error ::NotImplementedError
+ end
+ end
+
+ context 'with projects DataTransferResolver' do
+ let(:source) { described_class.project.source }
+
+ it 'outputs "Project"' do
+ expect(source).to eq 'Project'
+ end
+ end
+
+ context 'with groups DataTransferResolver' do
+ let(:source) { described_class.group.source }
+
+ it 'outputs "Group"' do
+ expect(source).to eq 'Group'
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/group_releases_resolver_spec.rb b/spec/graphql/resolvers/group_releases_resolver_spec.rb
new file mode 100644
index 00000000000..73386870030
--- /dev/null
+++ b/spec/graphql/resolvers/group_releases_resolver_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::GroupReleasesResolver, feature_category: :release_orchestration do
+ include GraphqlHelpers
+
+ let_it_be(:today) { Time.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :private, namespace: group) }
+ let_it_be(:release_v1) do
+ create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow)
+ end
+
+ let_it_be(:release_v2) do
+ create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday)
+ end
+
+ let_it_be(:release_v3) do
+ create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today)
+ end
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_user) { create(:user) }
+
+ let(:args) { { sort: :released_at_desc } }
+ let(:all_releases) { [release_v1, release_v2, release_v3] }
+
+ before do
+ group.add_member(developer, :owner)
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ it_behaves_like 'releases and group releases resolver'
+ end
+
+ private
+
+ def resolve_releases
+ context = { current_user: current_user }
+ resolve(described_class, obj: group, args: args, ctx: context, arg_style: :internal)
+ end
+end
diff --git a/spec/graphql/resolvers/groups_resolver_spec.rb b/spec/graphql/resolvers/groups_resolver_spec.rb
index e53ca674163..9d1ad46ed0e 100644
--- a/spec/graphql/resolvers/groups_resolver_spec.rb
+++ b/spec/graphql/resolvers/groups_resolver_spec.rb
@@ -2,131 +2,42 @@
require 'spec_helper'
-RSpec.describe Resolvers::GroupsResolver do
+RSpec.describe Resolvers::GroupsResolver, feature_category: :subgroups do
include GraphqlHelpers
describe '#resolve' do
- let_it_be(:group) { create(:group, name: 'public-group') }
- let_it_be(:private_group) { create(:group, :private, name: 'private-group') }
- let_it_be(:subgroup1) { create(:group, parent: group, name: 'Subgroup') }
- let_it_be(:subgroup2) { create(:group, parent: subgroup1, name: 'Test Subgroup 2') }
- let_it_be(:private_subgroup1) { create(:group, :private, parent: private_group, name: 'Subgroup1') }
- let_it_be(:private_subgroup2) { create(:group, :private, parent: private_subgroup1, name: 'Subgroup2') }
let_it_be(:user) { create(:user) }
+ let_it_be(:public_group) { create(:group, name: 'public-group') }
+ let_it_be(:private_group) { create(:group, :private, name: 'private-group') }
- before_all do
- private_group.add_developer(user)
- end
+ let(:params) { {} }
- shared_examples 'access to all public descendant groups' do
- it 'returns all public descendant groups of the parent group ordered by ASC name' do
- is_expected.to eq([subgroup1, subgroup2])
- end
- end
+ subject { resolve(described_class, args: params, ctx: { current_user: user }) }
- shared_examples 'access to all public subgroups' do
- it 'returns all public subgroups of the parent group' do
- is_expected.to contain_exactly(subgroup1)
- end
+ it 'includes public groups' do
+ expect(subject).to contain_exactly(public_group)
end
- shared_examples 'returning empty results' do
- it 'returns empty results' do
- is_expected.to be_empty
- end
+ it 'includes accessible private groups' do
+ private_group.add_developer(user)
+ expect(subject).to contain_exactly(public_group, private_group)
end
- context 'when parent group is public' do
- subject { resolve(described_class, obj: group, args: params, ctx: { current_user: current_user }) }
-
- context 'when `include_parent_descendants` is false' do
- let(:params) { { include_parent_descendants: false } }
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it_behaves_like 'access to all public subgroups'
- end
+ describe 'ordering' do
+ let_it_be(:other_group) { create(:group, name: 'other-group') }
- context 'when user is logged in' do
- let(:current_user) { user }
-
- it_behaves_like 'access to all public subgroups'
- end
- end
-
- context 'when `include_parent_descendants` is true' do
- let(:params) { { include_parent_descendants: true } }
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it_behaves_like 'access to all public descendant groups'
- end
-
- context 'when user is logged in' do
- let(:current_user) { user }
-
- it_behaves_like 'access to all public descendant groups'
-
- context 'with owned argument set as true' do
- before do
- subgroup1.add_owner(current_user)
- params[:owned] = true
- end
-
- it 'returns only descendant groups owned by the user' do
- is_expected.to contain_exactly(subgroup1)
- end
- end
-
- context 'with search argument' do
- it 'returns only descendant groups with matching name or path' do
- params[:search] = 'Test'
- is_expected.to contain_exactly(subgroup2)
- end
- end
- end
+ it 'orders by name ascending' do
+ expect(subject.map(&:name)).to eq(%w[other-group public-group])
end
end
- context 'when parent group is private' do
- subject { resolve(described_class, obj: private_group, args: params, ctx: { current_user: current_user }) }
-
- context 'when `include_parent_descendants` is true' do
- let(:params) { { include_parent_descendants: true } }
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it_behaves_like 'returning empty results'
- end
-
- context 'when user is logged in' do
- let(:current_user) { user }
-
- it 'returns all private descendant groups' do
- is_expected.to contain_exactly(private_subgroup1, private_subgroup2)
- end
- end
- end
-
- context 'when `include_parent_descendants` is false' do
- let(:params) { { include_parent_descendants: false } }
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it_behaves_like 'returning empty results'
- end
+ context 'with `search` argument' do
+ let_it_be(:other_group) { create(:group, name: 'other-group') }
- context 'when user is logged in' do
- let(:current_user) { user }
+ let(:params) { { search: 'oth' } }
- it 'returns private subgroups' do
- is_expected.to contain_exactly(private_subgroup1)
- end
- end
+ it 'filters groups by name' do
+ expect(subject).to contain_exactly(other_group)
end
end
end
diff --git a/spec/graphql/resolvers/nested_groups_resolver_spec.rb b/spec/graphql/resolvers/nested_groups_resolver_spec.rb
new file mode 100644
index 00000000000..e58edc3fd4b
--- /dev/null
+++ b/spec/graphql/resolvers/nested_groups_resolver_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::NestedGroupsResolver, feature_category: :subgroups do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:group) { create(:group, name: 'public-group') }
+ let_it_be(:private_group) { create(:group, :private, name: 'private-group') }
+ let_it_be(:subgroup1) { create(:group, parent: group, name: 'Subgroup') }
+ let_it_be(:subgroup2) { create(:group, parent: subgroup1, name: 'Test Subgroup 2') }
+ let_it_be(:private_subgroup1) { create(:group, :private, parent: private_group, name: 'Subgroup1') }
+ let_it_be(:private_subgroup2) { create(:group, :private, parent: private_subgroup1, name: 'Subgroup2') }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ private_group.add_developer(user)
+ end
+
+ shared_examples 'access to all public descendant groups' do
+ it 'returns all public descendant groups of the parent group ordered by ASC name' do
+ is_expected.to eq([subgroup1, subgroup2])
+ end
+ end
+
+ shared_examples 'access to all public subgroups' do
+ it 'returns all public subgroups of the parent group' do
+ is_expected.to contain_exactly(subgroup1)
+ end
+ end
+
+ shared_examples 'returning empty results' do
+ it 'returns empty results' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when parent group is public' do
+ subject { resolve(described_class, obj: group, args: params, ctx: { current_user: current_user }) }
+
+ context 'when `include_parent_descendants` is false' do
+ let(:params) { { include_parent_descendants: false } }
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'access to all public subgroups'
+ end
+
+ context 'when user is logged in' do
+ let(:current_user) { user }
+
+ it_behaves_like 'access to all public subgroups'
+ end
+ end
+
+ context 'when `include_parent_descendants` is true' do
+ let(:params) { { include_parent_descendants: true } }
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'access to all public descendant groups'
+ end
+
+ context 'when user is logged in' do
+ let(:current_user) { user }
+
+ it_behaves_like 'access to all public descendant groups'
+
+ context 'with owned argument set as true' do
+ before do
+ subgroup1.add_owner(current_user)
+ params[:owned] = true
+ end
+
+ it 'returns only descendant groups owned by the user' do
+ is_expected.to contain_exactly(subgroup1)
+ end
+ end
+
+ context 'with search argument' do
+ it 'returns only descendant groups with matching name or path' do
+ params[:search] = 'Test'
+ is_expected.to contain_exactly(subgroup2)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when parent group is private' do
+ subject { resolve(described_class, obj: private_group, args: params, ctx: { current_user: current_user }) }
+
+ context 'when `include_parent_descendants` is true' do
+ let(:params) { { include_parent_descendants: true } }
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'returning empty results'
+ end
+
+ context 'when user is logged in' do
+ let(:current_user) { user }
+
+ it 'returns all private descendant groups' do
+ is_expected.to contain_exactly(private_subgroup1, private_subgroup2)
+ end
+ end
+ end
+
+ context 'when `include_parent_descendants` is false' do
+ let(:params) { { include_parent_descendants: false } }
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'returning empty results'
+ end
+
+ context 'when user is logged in' do
+ let(:current_user) { user }
+
+ it 'returns private subgroups' do
+ is_expected.to contain_exactly(private_subgroup1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
index b95bab41e3e..6af2f56cef4 100644
--- a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::Projects::JiraProjectsResolver do
+RSpec.describe Resolvers::Projects::JiraProjectsResolver, feature_category: :integrations do
include GraphqlHelpers
specify do
diff --git a/spec/graphql/resolvers/releases_resolver_spec.rb b/spec/graphql/resolvers/releases_resolver_spec.rb
index 6ba9a6c33a1..58f6257c946 100644
--- a/spec/graphql/resolvers/releases_resolver_spec.rb
+++ b/spec/graphql/resolvers/releases_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::ReleasesResolver do
+RSpec.describe Resolvers::ReleasesResolver, feature_category: :release_orchestration do
include GraphqlHelpers
let_it_be(:today) { Time.now }
@@ -24,60 +24,28 @@ RSpec.describe Resolvers::ReleasesResolver do
end
describe '#resolve' do
- context 'when the user does not have access to the project' do
- let(:current_user) { public_user }
+ it_behaves_like 'releases and group releases resolver'
- it 'returns an empty array' do
- expect(resolve_releases).to be_empty
- end
- end
-
- context "when the user has full access to the project's releases" do
+ describe 'when order_by is created_at' do
let(:current_user) { developer }
- it 'returns all releases associated to the project' do
- expect(resolve_releases).to match_array(all_releases)
- end
-
- describe 'sorting behavior' do
- context 'with sort: :released_at_desc' do
- let(:args) { { sort: :released_at_desc } }
-
- it 'returns the releases ordered by released_at in descending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:released_at, :desc)
- end
- end
-
- context 'with sort: :released_at_asc' do
- let(:args) { { sort: :released_at_asc } }
-
- it 'returns the releases ordered by released_at in ascending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:released_at, :asc)
- end
- end
-
- context 'with sort: :created_desc' do
- let(:args) { { sort: :created_desc } }
+ context 'with sort: desc' do
+ let(:args) { { sort: :created_desc } }
- it 'returns the releases ordered by created_at in descending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:created_at, :desc)
- end
+ it 'returns the releases ordered by created_at in descending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :desc)
end
+ end
- context 'with sort: :created_asc' do
- let(:args) { { sort: :created_asc } }
+ context 'with sort: asc' do
+ let(:args) { { sort: :created_asc } }
- it 'returns the releases ordered by created_at in ascending order' do
- expect(resolve_releases.to_a)
- .to match_array(all_releases)
- .and be_sorted(:created_at, :asc)
- end
+ it 'returns the releases ordered by created_at in ascending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:created_at, :asc)
end
end
end
diff --git a/spec/graphql/resolvers/saved_reply_resolver_spec.rb b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
new file mode 100644
index 00000000000..f1cb0ca5214
--- /dev/null
+++ b/spec/graphql/resolvers/saved_reply_resolver_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::SavedReplyResolver, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:saved_reply) { create(:saved_reply, user: current_user) }
+
+ describe 'feature flag disabled' do
+ before do
+ stub_feature_flags(saved_replies: false)
+ end
+
+ it 'does not return saved reply' do
+ expect(resolve_saved_reply).to be_nil
+ end
+ end
+
+ describe 'feature flag enabled' do
+ it 'returns users saved reply' do
+ expect(resolve_saved_reply).to eq(saved_reply)
+ end
+
+ it 'returns nil when saved reply is not found' do
+ expect(resolve_saved_reply({ id: 'gid://gitlab/Users::SavedReply/100' })).to be_nil
+ end
+
+ it 'returns nil when saved reply is another users' do
+ other_users_saved_reply = create(:saved_reply, user: create(:user))
+
+ expect(resolve_saved_reply({ id: other_users_saved_reply.to_global_id })).to be_nil
+ end
+ end
+
+ def resolve_saved_reply(args = { id: saved_reply.to_global_id })
+ resolve(described_class, args: args, ctx: { current_user: current_user })
+ end
+end
diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb
index 27c3b9643ce..224213d1521 100644
--- a/spec/graphql/resolvers/users/participants_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb
@@ -115,8 +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
- # 1 extra query to load work item widgets collection
- expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(6)
+ # 2 extra queries to load work item widgets collection
+ expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(7)
end
it 'does not execute N+1 for system note metadata relation' do
diff --git a/spec/graphql/resolvers/work_items_resolver_spec.rb b/spec/graphql/resolvers/work_items_resolver_spec.rb
index d89ccc7f806..6da62e3adb7 100644
--- a/spec/graphql/resolvers/work_items_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_items_resolver_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe Resolvers::WorkItemsResolver do
end
it 'batches queries that only include IIDs', :request_store do
- result = batch_sync(max_queries: 7) do
+ result = batch_sync(max_queries: 8) do
[item1, item2]
.map { |item| resolve_items(iid: item.iid.to_s) }
.flat_map(&:to_a)
@@ -111,7 +111,7 @@ RSpec.describe Resolvers::WorkItemsResolver do
end
it 'finds a specific item with iids', :request_store do
- result = batch_sync(max_queries: 7) do
+ result = batch_sync(max_queries: 8) do
resolve_items(iids: [item1.iid]).to_a
end
diff --git a/spec/graphql/types/achievements/achievement_type_spec.rb b/spec/graphql/types/achievements/achievement_type_spec.rb
index 5c98753ac66..f967dc8e25e 100644
--- a/spec/graphql/types/achievements/achievement_type_spec.rb
+++ b/spec/graphql/types/achievements/achievement_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Achievement'], feature_category: :users do
+RSpec.describe GitlabSchema.types['Achievement'], feature_category: :user_profile do
include GraphqlHelpers
let(:fields) do
@@ -12,7 +12,6 @@ RSpec.describe GitlabSchema.types['Achievement'], feature_category: :users do
name
avatar_url
description
- revokeable
created_at
updated_at
]
diff --git a/spec/graphql/types/ci/job_token_scope_type_spec.rb b/spec/graphql/types/ci/job_token_scope_type_spec.rb
index 569b59d6c70..01044364881 100644
--- a/spec/graphql/types/ci/job_token_scope_type_spec.rb
+++ b/spec/graphql/types/ci/job_token_scope_type_spec.rb
@@ -2,17 +2,24 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do
+RSpec.describe GitlabSchema.types['CiJobTokenScopeType'], feature_category: :continuous_integration do
specify { expect(described_class.graphql_name).to eq('CiJobTokenScopeType') }
it 'has the correct fields' do
- expected_fields = [:projects]
+ expected_fields = [:projects, :inboundAllowlist, :outboundAllowlist]
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe 'query' do
- let(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
+ let(:project) do
+ create(
+ :project,
+ ci_outbound_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ ).tap(&:save!)
+ end
+
let_it_be(:current_user) { create(:user) }
let(:query) do
@@ -25,6 +32,16 @@ RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do
path
}
}
+ inboundAllowlist {
+ nodes {
+ path
+ }
+ }
+ outboundAllowlist {
+ nodes {
+ path
+ }
+ }
}
}
}
@@ -33,30 +50,73 @@ RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do
subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
- let(:projects_field) { subject.dig('data', 'project', 'ciJobTokenScope', 'projects', 'nodes') }
- let(:returned_project_paths) { projects_field.map { |project| project['path'] } }
+ let(:scope_field) { subject.dig('data', 'project', 'ciJobTokenScope') }
+ let(:errors_field) { subject['errors'] }
+ let(:projects_field) { scope_field&.dig('projects', 'nodes') }
+ let(:outbound_allowlist_field) { scope_field&.dig('outboundAllowlist', 'nodes') }
+ let(:inbound_allowlist_field) { scope_field&.dig('inboundAllowlist', 'nodes') }
+ let(:returned_project_paths) { projects_field.map { |p| p['path'] } }
+ let(:returned_outbound_paths) { outbound_allowlist_field.map { |p| p['path'] } }
+ let(:returned_inbound_paths) { inbound_allowlist_field.map { |p| p['path'] } }
+
+ context 'without access to scope' do
+ before do
+ project.add_member(current_user, :developer)
+ end
+
+ it 'returns no projects' do
+ expect(projects_field).to be_nil
+ expect(outbound_allowlist_field).to be_nil
+ expect(inbound_allowlist_field).to be_nil
+ expect(errors_field.first['message']).to include "don't have permission"
+ end
+ end
context 'with access to scope' do
before do
project.add_member(current_user, :maintainer)
end
- context 'when multiple projects in the allow list' do
- let!(:link) { create(:ci_job_token_project_scope_link, source_project: project) }
+ context 'when multiple projects in the allow lists' do
+ include Ci::JobTokenScopeHelpers
+ let!(:outbound_allowlist_project) { create_project_in_allowlist(project, direction: :outbound) }
+ let!(:inbound_allowlist_project) { create_project_in_allowlist(project, direction: :inbound) }
+ let!(:both_allowlists_project) { create_project_in_both_allowlists(project) }
context 'when linked projects are readable' do
before do
- link.target_project.add_member(current_user, :developer)
+ outbound_allowlist_project.add_member(current_user, :developer)
+ inbound_allowlist_project.add_member(current_user, :developer)
+ both_allowlists_project.add_member(current_user, :developer)
end
- it 'returns readable projects in scope' do
- expect(returned_project_paths).to contain_exactly(project.path, link.target_project.path)
+ shared_examples 'returns projects' do
+ it 'returns readable projects in scope' do
+ outbound_paths = [project.path, outbound_allowlist_project.path, both_allowlists_project.path]
+ inbound_paths = [project.path, inbound_allowlist_project.path, both_allowlists_project.path]
+
+ expect(returned_project_paths).to contain_exactly(*outbound_paths)
+ expect(returned_outbound_paths).to contain_exactly(*outbound_paths)
+ expect(returned_inbound_paths).to contain_exactly(*inbound_paths)
+ end
+ end
+
+ it_behaves_like 'returns projects'
+
+ context 'when job token scope is disabled' do
+ before do
+ project.ci_cd_settings.update!(job_token_scope_enabled: false)
+ end
+
+ it_behaves_like 'returns projects'
end
end
- context 'when linked project is not readable' do
+ context 'when linked projects are not readable' do
it 'returns readable projects in scope' do
expect(returned_project_paths).to contain_exactly(project.path)
+ expect(returned_outbound_paths).to contain_exactly(project.path)
+ expect(returned_inbound_paths).to contain_exactly(project.path)
end
end
@@ -71,6 +131,8 @@ RSpec.describe GitlabSchema.types['CiJobTokenScopeType'] do
it 'returns readable projects in scope' do
expect(returned_project_paths).to contain_exactly(project.path)
+ expect(returned_outbound_paths).to contain_exactly(project.path)
+ expect(returned_inbound_paths).to contain_exactly(project.path)
end
end
end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index ce1558c4097..714eaebfe73 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Types::Ci::JobType do
detailedStatus
duration
downstreamPipeline
+ erasedAt
finished_at
id
kind
@@ -32,6 +33,7 @@ RSpec.describe Types::Ci::JobType do
pipeline
playable
previousStageJobsOrNeeds
+ project
queued_at
queued_duration
refName
diff --git a/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb b/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb
index 1c98539e308..2e5e6ad204f 100644
--- a/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_schedule_variable_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::PipelineScheduleVariableType do
+RSpec.describe Types::Ci::PipelineScheduleVariableType, feature_category: :continuous_integration 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) }
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 5683b3f86c4..67209874b54 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Types::Ci::PipelineType do
if Gitlab.ee?
expected_fields += %w[
security_report_summary security_report_findings security_report_finding
- code_quality_reports dast_profile
+ code_quality_reports dast_profile code_quality_report_summary
]
end
diff --git a/spec/graphql/types/ci/runner_architecture_type_spec.rb b/spec/graphql/types/ci/runner_architecture_type_spec.rb
index 60709acfd53..58ec73323c6 100644
--- a/spec/graphql/types/ci/runner_architecture_type_spec.rb
+++ b/spec/graphql/types/ci/runner_architecture_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerArchitectureType do
+RSpec.describe Types::Ci::RunnerArchitectureType, feature_category: :runner do
specify { expect(described_class.graphql_name).to eq('RunnerArchitecture') }
it 'exposes the expected fields' do
diff --git a/spec/graphql/types/ci/runner_platform_type_spec.rb b/spec/graphql/types/ci/runner_platform_type_spec.rb
index 29b8e885183..1b0f5a5ec71 100644
--- a/spec/graphql/types/ci/runner_platform_type_spec.rb
+++ b/spec/graphql/types/ci/runner_platform_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerPlatformType do
+RSpec.describe Types::Ci::RunnerPlatformType, feature_category: :runner_fleet do
specify { expect(described_class.graphql_name).to eq('RunnerPlatform') }
it 'exposes the expected fields' do
diff --git a/spec/graphql/types/ci/runner_setup_type_spec.rb b/spec/graphql/types/ci/runner_setup_type_spec.rb
index 197e717e964..d3e47b52a80 100644
--- a/spec/graphql/types/ci/runner_setup_type_spec.rb
+++ b/spec/graphql/types/ci/runner_setup_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerSetupType do
+RSpec.describe Types::Ci::RunnerSetupType, feature_category: :runner_fleet do
specify { expect(described_class.graphql_name).to eq('RunnerSetup') }
it 'exposes the expected fields' do
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index b078d7f5fac..a2d107ae295 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['CiRunner'] do
+RSpec.describe GitlabSchema.types['CiRunner'], feature_category: :runner do
specify { expect(described_class.graphql_name).to eq('CiRunner') }
specify { expect(described_class).to require_graphql_authorizations(:read_runner) }
@@ -13,6 +13,7 @@ RSpec.describe GitlabSchema.types['CiRunner'] do
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 job_execution_status
+ ephemeral_authentication_token
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb b/spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb
index ef378f3fc5a..4aa9ad094a6 100644
--- a/spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb
+++ b/spec/graphql/types/ci/runner_upgrade_status_enum_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerUpgradeStatusEnum do
+RSpec.describe Types::Ci::RunnerUpgradeStatusEnum, feature_category: :runner_fleet do
let(:model_only_enum_values) { %w[not_processed] }
let(:expected_graphql_source_values) do
Ci::RunnerVersion.statuses.keys - model_only_enum_values
@@ -15,6 +15,7 @@ RSpec.describe Types::Ci::RunnerUpgradeStatusEnum do
expected_graphql_source_values
.map(&:upcase)
.map { |v| v == 'INVALID_VERSION' ? 'INVALID' : v }
+ .map { |v| v == 'UNAVAILABLE' ? 'NOT_AVAILABLE' : v }
)
end
diff --git a/spec/graphql/types/ci/runner_web_url_edge_spec.rb b/spec/graphql/types/ci/runner_web_url_edge_spec.rb
index 08718df0a5b..07a9655b3e1 100644
--- a/spec/graphql/types/ci/runner_web_url_edge_spec.rb
+++ b/spec/graphql/types/ci/runner_web_url_edge_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerWebUrlEdge do
+RSpec.describe Types::Ci::RunnerWebUrlEdge, feature_category: :runner_fleet do
specify { expect(described_class.graphql_name).to eq('RunnerWebUrlEdge') }
it 'contains URL attributes' do
diff --git a/spec/graphql/types/ci/variable_sort_enum_spec.rb b/spec/graphql/types/ci/variable_sort_enum_spec.rb
new file mode 100644
index 00000000000..1702360a21f
--- /dev/null
+++ b/spec/graphql/types/ci/variable_sort_enum_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::VariableSortEnum, feature_category: :pipeline_authoring do
+ it 'exposes the available order methods' do
+ expect(described_class.values).to match(
+ 'KEY_ASC' => have_attributes(value: :key_asc),
+ 'KEY_DESC' => have_attributes(value: :key_desc)
+ )
+ end
+end
diff --git a/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
index cb7ce19c9fc..a0d99f5f0c1 100644
--- a/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
+++ b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['VerificationStatus'] do
.to match_array(%w[
UNVERIFIED UNVERIFIED_KEY VERIFIED
SAME_USER_DIFFERENT_EMAIL OTHER_USER UNKNOWN_KEY
- MULTIPLE_SIGNATURES
+ MULTIPLE_SIGNATURES REVOKED_KEY
])
end
end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 0b65778ce90..6820cf2738e 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe GitlabSchema.types['Group'] do
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
shared_runners_setting timelogs organization_state_counts organizations
contact_state_counts contacts work_item_types
- recent_issue_boards ci_variables
+ recent_issue_boards ci_variables releases
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -70,6 +70,13 @@ RSpec.describe GitlabSchema.types['Group'] do
it { is_expected.to have_graphql_resolver(Resolvers::Crm::OrganizationStateCountsResolver) }
end
+ describe 'releases field' do
+ subject { described_class.fields['releases'] }
+
+ it { is_expected.to have_graphql_type(Types::ReleaseType.connection_type) }
+ it { is_expected.to have_graphql_resolver(Resolvers::GroupReleasesResolver) }
+ end
+
it_behaves_like 'a GraphQL type with labels' do
let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] }
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 498625dc642..7c6cf137a1e 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -228,29 +228,17 @@ RSpec.describe GitlabSchema.types['Issue'] do
subject { GitlabSchema.execute(query, context: { current_user: admin }).as_json }
- context 'when `ban_user_feature_flag` is enabled' do
- context 'when issue is hidden' do
- it 'returns `true`' do
- expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(true)
- end
- end
-
- context 'when issue is visible' do
- let(:issue) { visible_issue }
-
- it 'returns `false`' do
- expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(false)
- end
+ context 'when issue is hidden' do
+ it 'returns `true`' do
+ expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(true)
end
end
- context 'when `ban_user_feature_flag` is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
+ context 'when issue is visible' do
+ let(:issue) { visible_issue }
- it 'returns `nil`' do
- expect(subject.dig('data', 'project', 'issue', 'hidden')).to be_nil
+ it 'returns `false`' do
+ expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(false)
end
end
end
diff --git a/spec/graphql/types/notes/deleted_note_type_spec.rb b/spec/graphql/types/notes/deleted_note_type_spec.rb
new file mode 100644
index 00000000000..70985484e75
--- /dev/null
+++ b/spec/graphql/types/notes/deleted_note_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DeletedNote'], feature_category: :team_planning do
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ id
+ discussion_id
+ last_discussion_note
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).only
+ end
+end
diff --git a/spec/graphql/types/packages/package_details_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb
index d5688fc64c5..e4fe53f7660 100644
--- a/spec/graphql/types/packages/package_details_type_spec.rb
+++ b/spec/graphql/types/packages/package_details_type_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['PackageDetailsType'] do
it 'includes all the package fields' do
expected_fields = %w[
id name version created_at updated_at package_type tags project
- pipelines versions package_files dependency_links
+ pipelines versions package_files dependency_links public_package
npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
composer_url composer_config_repository_url
]
diff --git a/spec/graphql/types/permission_types/merge_request_spec.rb b/spec/graphql/types/permission_types/merge_request_spec.rb
index 2849dead9a8..2c5da9a933c 100644
--- a/spec/graphql/types/permission_types/merge_request_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Types::PermissionTypes::MergeRequest do
:read_merge_request, :admin_merge_request, :update_merge_request,
:create_note, :push_to_source_branch, :remove_source_branch,
:cherry_pick_on_current_merge_request, :revert_on_current_merge_request,
- :can_merge
+ :can_merge, :can_approve
]
expect(described_class).to have_graphql_fields(expected_permissions)
diff --git a/spec/graphql/types/permission_types/work_item_spec.rb b/spec/graphql/types/permission_types/work_item_spec.rb
index e604ce5d6e0..db6d78b1538 100644
--- a/spec/graphql/types/permission_types/work_item_spec.rb
+++ b/spec/graphql/types/permission_types/work_item_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::WorkItem do
it do
expected_permissions = [
- :read_work_item, :update_work_item, :delete_work_item
+ :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
]
expected_permissions.each do |permission|
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 4151789372b..7f26190830e 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Project'] do
include GraphqlHelpers
+ include ProjectForksHelper
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
@@ -37,7 +38,7 @@ RSpec.describe GitlabSchema.types['Project'] do
ci_template timelogs merge_commit_template squash_commit_template work_item_types
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
- incident_management_timeline_event_tags
+ incident_management_timeline_event_tags visible_forks
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -285,6 +286,17 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ context 'with empty repository' do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ it 'raises an error' do
+ expect(subject['errors'][0]['message']).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ 'href="http://localhost/help/user/project/repository/index.md#' \
+ 'add-files-to-a-repository">add at least one file to the ' \
+ 'repository</a> before using Security features.')
+ end
+ end
end
describe 'issue field' do
@@ -848,4 +860,54 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ describe 'visible_forks' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:fork_reporter) { fork_project(project, nil, { repository: true }) }
+ let_it_be(:fork_developer) { fork_project(project, nil, { repository: true }) }
+ let_it_be(:fork_group_developer) { fork_project(project, nil, { repository: true }) }
+ let_it_be(:fork_public) { fork_project(project, nil, { repository: true }) }
+ let_it_be(:fork_private) { fork_project(project, nil, { repository: true }) }
+
+ let(:minimum_access_level) { '' }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ visibleForks#{minimum_access_level} {
+ nodes {
+ fullPath
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:forks) do
+ subject.dig('data', 'project', 'visibleForks', 'nodes')
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ before do
+ fork_reporter.add_reporter(user)
+ fork_developer.add_developer(user)
+ fork_group_developer.group.add_developer(user)
+ end
+
+ it 'contains all forks' do
+ expect(forks.count).to eq(5)
+ end
+
+ context 'with minimum_access_level DEVELOPER' do
+ let(:minimum_access_level) { '(minimumAccessLevel: DEVELOPER)' }
+
+ it 'contains forks with developer access' do
+ expect(forks).to contain_exactly(a_hash_including('fullPath' => fork_developer.full_path),
+a_hash_including('fullPath' => fork_group_developer.full_path))
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index f06759e30c8..100ecc94f35 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -2,49 +2,15 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Query'] do
+RSpec.describe GitlabSchema.types['Query'], feature_category: :shared do
+ include_context 'with FOSS query type fields'
+
it 'is called Query' do
expect(described_class.graphql_name).to eq('Query')
end
it 'has the expected fields' do
- expected_fields = [
- :board_list,
- :ci_application_settings,
- :ci_config,
- :ci_variables,
- :container_repository,
- :current_user,
- :design_management,
- :echo,
- :gitpod_enabled,
- :group,
- :issue,
- :issues,
- :jobs,
- :merge_request,
- :metadata,
- :milestone,
- :namespace,
- :package,
- :project,
- :projects,
- :query_complexity,
- :runner,
- :runner_platforms,
- :runner_setup,
- :runners,
- :snippets,
- :timelogs,
- :todo,
- :topics,
- :usage_trends_measurements,
- :user,
- :users,
- :work_item
- ]
-
- expect(described_class).to have_graphql_fields(*expected_fields).at_least
+ expect(described_class).to have_graphql_fields(*expected_foss_fields).at_least
end
describe 'namespace field' do
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index 45cb960cf20..a6b5d454b60 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['User'], feature_category: :users do
+RSpec.describe GitlabSchema.types['User'], feature_category: :user_profile do
specify { expect(described_class.graphql_name).to eq('User') }
specify do
@@ -46,6 +46,7 @@ RSpec.describe GitlabSchema.types['User'], feature_category: :users do
preferencesGitpodPath
profileEnableGitpodPath
savedReplies
+ savedReply
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/users/email_type_spec.rb b/spec/graphql/types/users/email_type_spec.rb
index fb484915428..107bbf81e98 100644
--- a/spec/graphql/types/users/email_type_spec.rb
+++ b/spec/graphql/types/users/email_type_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Email'], feature_category: :users do
+RSpec.describe GitlabSchema.types['Email'], feature_category: :user_profile do
it 'has the correct fields' do
expected_fields = [
:id,
diff --git a/spec/graphql/types/users/namespace_commit_email_type_spec.rb b/spec/graphql/types/users/namespace_commit_email_type_spec.rb
index ccab881676e..27e50f7285e 100644
--- a/spec/graphql/types/users/namespace_commit_email_type_spec.rb
+++ b/spec/graphql/types/users/namespace_commit_email_type_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe GitlabSchema.types['NamespaceCommitEmail'], feature_category: :users do
+RSpec.describe GitlabSchema.types['NamespaceCommitEmail'], feature_category: :user_profile do
it 'has the correct fields' do
expected_fields = [
:id,
diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb
index dded96fde3a..42d56598944 100644
--- a/spec/graphql/types/work_item_type_spec.rb
+++ b/spec/graphql/types/work_item_type_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['WorkItem'] do
it 'has specific fields' do
fields = %i[
+ author
confidential
description
description_html
diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb
index 380df49cde3..d47127d9661 100644
--- a/spec/haml_lint/linter/documentation_links_spec.rb
+++ b/spec/haml_lint/linter/documentation_links_spec.rb
@@ -6,7 +6,7 @@ require 'haml_lint/spec'
require_relative '../../../haml_lint/linter/documentation_links'
-RSpec.describe HamlLint::Linter::DocumentationLinks do
+RSpec.describe HamlLint::Linter::DocumentationLinks, feature_category: :tooling do
include_context 'linter'
shared_examples 'link validation rules' do |link_pattern|
@@ -95,11 +95,8 @@ RSpec.describe HamlLint::Linter::DocumentationLinks do
end
end
- context 'help_page_path' do
- it_behaves_like 'link validation rules', 'help_page_path'
- end
-
- context 'help_page_url' do
- it_behaves_like 'link validation rules', 'help_page_url'
- end
+ it_behaves_like 'link validation rules', 'help_page_path'
+ it_behaves_like 'link validation rules', 'help_page_url'
+ it_behaves_like 'link validation rules', 'Rails.application.routes.url_helpers.help_page_url'
+ it_behaves_like 'link validation rules', 'Gitlab::Routing.url_helpers.help_page_url'
end
diff --git a/spec/helpers/admin/user_actions_helper_spec.rb b/spec/helpers/admin/user_actions_helper_spec.rb
index 3bc380fbc99..87d2308690c 100644
--- a/spec/helpers/admin/user_actions_helper_spec.rb
+++ b/spec/helpers/admin/user_actions_helper_spec.rb
@@ -114,23 +114,5 @@ RSpec.describe Admin::UserActionsHelper do
it { is_expected.to match_array([]) }
end
-
- context 'when `ban_user_feature_flag` is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- context 'the user is a standard user' do
- let_it_be(:user) { create(:user) }
-
- it { is_expected.not_to include("ban") }
- end
-
- context 'the user is banned' do
- let_it_be(:user) { create(:user, :banned) }
-
- it { is_expected.not_to include("unban") }
- end
- end
end
end
diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb
index 8673353996e..2b0192d24b3 100644
--- a/spec/helpers/appearances_helper_spec.rb
+++ b/spec/helpers/appearances_helper_spec.rb
@@ -10,17 +10,90 @@ RSpec.describe AppearancesHelper do
allow(helper).to receive(:current_user).and_return(user)
end
- describe '#appearance_short_name' do
+ describe 'pwa icon scaled' do
+ before do
+ stub_config_setting(relative_url_root: '/relative_root')
+ end
+
+ shared_examples 'gets icon path' do |width|
+ let!(:width) { width }
+
+ it 'returns path of icon' do
+ expect(helper.appearance_pwa_icon_path_scaled(width)).to match(result)
+ end
+ end
+
+ context 'with custom icon' do
+ let!(:appearance) { create(:appearance, :with_pwa_icon) }
+ let!(:result) { "/relative_root/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=#{width}" }
+
+ it_behaves_like 'gets icon path', 192
+ it_behaves_like 'gets icon path', 512
+ end
+
+ context 'with default icon' do
+ let!(:result) { "/relative_root/-/pwa-icons/logo-#{width}.png" }
+
+ it_behaves_like 'gets icon path', 192
+ it_behaves_like 'gets icon path', 512
+ end
+
+ it 'returns path of maskable logo' do
+ expect(helper.appearance_maskable_logo).to match('/relative_root/-/pwa-icons/maskable-logo.png')
+ end
+
+ context 'with wrong input' do
+ let!(:result) { nil }
+
+ it_behaves_like 'gets icon path', 19200
+ end
+
+ context 'when path is append to root' do
+ it 'appends root and path' do
+ expect(helper.append_root_path('/works_just_fine')).to match('/relative_root/works_just_fine')
+ end
+ end
+ end
+
+ describe '#appearance_pwa_name' do
it 'returns the default value' do
create(:appearance)
- expect(helper.appearance_short_name).to match('GitLab')
+ expect(helper.appearance_pwa_name).to match('GitLab')
+ end
+
+ it 'returns the customized value' do
+ create(:appearance, pwa_name: 'GitLab as PWA')
+
+ expect(helper.appearance_pwa_name).to match('GitLab as PWA')
+ end
+ end
+
+ describe '#appearance_pwa_short_name' do
+ it 'returns the default value' do
+ create(:appearance)
+
+ expect(helper.appearance_pwa_short_name).to match('GitLab')
end
it 'returns the customized value' do
create(:appearance, pwa_short_name: 'Short')
- expect(helper.appearance_short_name).to match('Short')
+ expect(helper.appearance_pwa_short_name).to match('Short')
+ end
+ end
+
+ describe '#appearance_pwa_description' do
+ it 'returns the default value' do
+ create(:appearance)
+
+ expect(helper.appearance_pwa_description).to include('The complete DevOps platform.')
+ end
+
+ it 'returns the customized value' do
+ create(:appearance, pwa_description: 'This is a description')
+
+ expect(helper.appearance_pwa_description).to match('This is a description')
end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index a8514c373db..bb1a4d57cc0 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -211,7 +211,7 @@ RSpec.describe ApplicationHelper do
describe '#support_url' do
context 'when alternate support url is specified' do
- let(:alternate_url) { 'http://company.example.com/getting-help' }
+ let(:alternate_url) { 'http://company.example.com/get-help' }
it 'returns the alternate support url' do
stub_application_setting(help_page_support_url: alternate_url)
@@ -222,7 +222,7 @@ RSpec.describe ApplicationHelper do
context 'when alternate support url is not specified' do
it 'builds the support url from the promo_url' do
- expect(helper.support_url).to eq(helper.promo_url + '/getting-help/')
+ expect(helper.support_url).to eq(helper.promo_url + '/get-help/')
end
end
end
@@ -540,6 +540,23 @@ RSpec.describe ApplicationHelper do
end
end
+ describe '#profile_social_links' do
+ context 'when discord is set' do
+ let_it_be(:user) { build(:user) }
+ let(:discord) { discord_url(user) }
+
+ it 'returns an empty string if discord is not set' do
+ expect(discord).to eq('')
+ end
+
+ it 'returns discord url when discord id is set' do
+ user.discord = '1234567890123456789'
+
+ expect(discord).to eq('https://discord.com/users/1234567890123456789')
+ end
+ end
+ end
+
describe '#gitlab_ui_form_for' do
let_it_be(:user) { build(:user) }
@@ -689,28 +706,4 @@ RSpec.describe ApplicationHelper do
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 914c866c464..19cb970553b 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -3,6 +3,14 @@
require 'spec_helper'
RSpec.describe ApplicationSettingsHelper do
+ include Devise::Test::ControllerHelpers
+
+ let_it_be(:current_user) { create(:admin) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ end
+
context 'when all protocols in use' do
before do
stub_application_setting(enabled_git_access_protocol: '')
@@ -360,13 +368,10 @@ RSpec.describe ApplicationSettingsHelper do
end
describe '#instance_clusters_enabled?', :request_store do
- let_it_be(:user) { create(:user) }
-
subject { helper.instance_clusters_enabled? }
before do
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?).with(user, :read_cluster, instance_of(Clusters::Instance)).and_return(true)
+ allow(helper).to receive(:can?).with(current_user, :read_cluster, instance_of(Clusters::Instance)).and_return(true)
end
it { is_expected.to be_truthy }
@@ -379,4 +384,52 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to be_falsey }
end
end
+
+ describe '#restricted_level_checkboxes' do
+ let_it_be(:application_setting) { create(:application_setting) }
+
+ before do
+ allow(current_user).to receive(:can_admin_all_resources?).and_return(true)
+ stub_application_setting(
+ restricted_visibility_levels: [
+ Gitlab::VisibilityLevel::PUBLIC,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PRIVATE
+ ]
+ )
+ end
+
+ it 'returns restricted level checkboxes with correct label, description, and HTML attributes' do
+ helper.gitlab_ui_form_for(application_setting, url: '/admin/application_settings/general') do |form|
+ result = helper.restricted_level_checkboxes(form)
+
+ expect(result[0]).to have_checked_field(s_('VisibilityLevel|Private'), with: Gitlab::VisibilityLevel::PRIVATE)
+ expect(result[0]).to have_selector('[data-testid="lock-icon"]')
+ expect(result[0]).to have_content(
+ s_(
+ 'AdminSettings|If selected, only administrators are able to create private groups, projects, and ' \
+ 'snippets.'
+ )
+ )
+
+ expect(result[1]).to have_checked_field(s_('VisibilityLevel|Internal'), with: Gitlab::VisibilityLevel::INTERNAL)
+ expect(result[1]).to have_selector('[data-testid="shield-icon"]')
+ expect(result[1]).to have_content(
+ s_(
+ 'AdminSettings|If selected, only administrators are able to create internal groups, projects, and ' \
+ 'snippets.'
+ )
+ )
+
+ expect(result[2]).to have_checked_field(s_('VisibilityLevel|Public'), with: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result[2]).to have_selector('[data-testid="earth-icon"]')
+ expect(result[2]).to have_content(
+ s_(
+ 'AdminSettings|If selected, only administrators are able to create public groups, projects, ' \
+ 'and snippets. Also, profiles are only visible to authenticated users.'
+ )
+ )
+ end
+ end
+ end
end
diff --git a/spec/helpers/artifacts_helper_spec.rb b/spec/helpers/artifacts_helper_spec.rb
new file mode 100644
index 00000000000..cf48f0ecc39
--- /dev/null
+++ b/spec/helpers/artifacts_helper_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe ArtifactsHelper, feature_category: :build_artifacts do
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:project) { build_stubbed(:project) }
+
+ describe '#artifacts_app_data' do
+ before do
+ allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).with(user, :destroy_artifacts, project).and_return(false)
+ end
+
+ subject { helper.artifacts_app_data(project) }
+
+ it 'returns expected data' do
+ expect(subject).to include({
+ project_path: project.full_path,
+ artifacts_management_feedback_image_path: match_asset_path('illustrations/chat-bubble-sm.svg')
+ })
+ end
+
+ describe 'can_destroy_artifacts' do
+ it 'returns false without permission' do
+ expect(subject[:can_destroy_artifacts]).to eq('false')
+ end
+
+ it 'returns true when user has permission' do
+ allow(helper).to receive(:can?).with(user, :destroy_artifacts, project).and_return(true)
+
+ expect(subject[:can_destroy_artifacts]).to eq('true')
+ end
+ end
+ end
+end
diff --git a/spec/helpers/bizible_helper_spec.rb b/spec/helpers/bizible_helper_spec.rb
index b82211d51ec..c1b79a8e1e2 100644
--- a/spec/helpers/bizible_helper_spec.rb
+++ b/spec/helpers/bizible_helper_spec.rb
@@ -4,43 +4,43 @@ require "spec_helper"
RSpec.describe BizibleHelper do
describe '#bizible_enabled?' do
- before do
- stub_config(extra: { bizible: SecureRandom.uuid })
- end
-
- context 'when bizible is disabled' do
+ context 'when bizible config is not true' do
before do
- allow(helper).to receive(:bizible_enabled?).and_return(false)
+ stub_config(extra: { bizible: false })
end
- it { is_expected.to be_falsey }
+ it { expect(helper.bizible_enabled?).to be_falsy }
end
- context 'when bizible is enabled' do
+ context 'when bizible config is enabled' do
before do
- allow(helper).to receive(:bizible_enabled?).and_return(true)
+ stub_config(extra: { bizible: true })
end
- it { is_expected.to be_truthy }
- end
+ it { expect(helper.bizible_enabled?).to be_truthy }
- subject(:bizible_enabled?) { helper.bizible_enabled? }
+ context 'with ecomm_instrumentation feature flag disabled' do
+ before do
+ stub_feature_flags(ecomm_instrumentation: false)
+ end
- context 'with ecomm_instrumentation feature flag disabled' do
- before do
- stub_feature_flags(ecomm_instrumentation: false)
+ it { expect(helper.bizible_enabled?).to be_falsey }
end
- it { is_expected.to be_falsey }
- end
+ context 'with ecomm_instrumentation feature flag enabled' do
+ before do
+ stub_feature_flags(ecomm_instrumentation: true)
+ end
+
+ it { expect(helper.bizible_enabled?).to be_truthy }
+ end
- context 'with ecomm_instrumentation feature flag enabled' do
- context 'when no id is set' do
+ context 'with invite_email present' do
before do
- stub_config(extra: {})
+ stub_feature_flags(ecomm_instrumentation: true)
end
- it { is_expected.to be_falsey }
+ it { expect(helper.bizible_enabled?('test@test.com')).to be_falsy }
end
end
end
diff --git a/spec/helpers/ci/variables_helper_spec.rb b/spec/helpers/ci/variables_helper_spec.rb
new file mode 100644
index 00000000000..d032e7f9087
--- /dev/null
+++ b/spec/helpers/ci/variables_helper_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::VariablesHelper, feature_category: :pipeline_authoring do
+ describe '#ci_variable_maskable_raw_regex' do
+ it 'converts to a javascript regex' do
+ expect(helper.ci_variable_maskable_raw_regex).to eq("^\\S{8,}$")
+ end
+ end
+end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 1f7400983da..dbc6bd2eb28 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -385,7 +385,7 @@ RSpec.describe EmailsHelper do
context 'with no html tag' do
let(:expected_output) do
- 'John was added as a reviewer.<br>'
+ 'John was added as a reviewer.'
end
it 'returns the expected output' do
@@ -395,7 +395,7 @@ RSpec.describe EmailsHelper do
context 'with <strong> tag' do
let(:expected_output) do
- '<strong>John</strong> was added as a reviewer.<br>'
+ '<strong>John</strong> was added as a reviewer.'
end
it 'returns the expected output' do
@@ -410,7 +410,7 @@ RSpec.describe EmailsHelper do
context 'with no html tag' do
let(:expected_output) do
- 'Ted was added as a reviewer.<br>John and Mary were removed from reviewers.'
+ "Ted was added as a reviewer.\nJohn and Mary were removed from reviewers."
end
it 'returns the expected output' do
@@ -460,7 +460,7 @@ RSpec.describe EmailsHelper do
let(:fishy_user) { build(:user, name: "<script>alert('hi')</script>") }
let(:expected_output) do
- '<strong>&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</strong> was added as a reviewer.<br>'
+ '<strong>&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</strong> was added as a reviewer.'
end
it 'escapes the html tag' do
@@ -476,7 +476,7 @@ RSpec.describe EmailsHelper do
let(:fishy_user) { build(:user, name: "example.com") }
let(:expected_output) do
- 'example_com was added as a reviewer.<br>'
+ 'example_com was added as a reviewer.'
end
it "sanitizes user's name" do
diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb
index 7c8c59be409..83b08e5fcec 100644
--- a/spec/helpers/form_helper_spec.rb
+++ b/spec/helpers/form_helper_spec.rb
@@ -6,38 +6,15 @@ RSpec.describe FormHelper do
include Devise::Test::ControllerHelpers
describe '#dropdown_max_select' do
- let(:feature_flag) { :limit_reviewer_and_assignee_size }
-
- context "with the :limit_reviewer_and_assignee_size feature flag on" do
- before do
- stub_feature_flags(feature_flag => true)
- end
-
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- max = Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
-
- expect(helper.dropdown_max_select({}, feature_flag))
- .to eq(max)
- expect(helper.dropdown_max_select({ 'max-select'.to_sym => 5 }, feature_flag))
- .to eq(5)
- expect(helper.dropdown_max_select({ 'max-select'.to_sym => max + 5 }, feature_flag))
- .to eq(max)
- end
- end
-
- context "with the :limit_reviewer_and_assignee_size feature flag off" do
- before do
- stub_feature_flags(feature_flag => false)
- end
-
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- expect(helper.dropdown_max_select({}, feature_flag))
- .to eq(nil)
- expect(helper.dropdown_max_select({ 'max-select'.to_sym => 5 }, feature_flag))
- .to eq(5)
- expect(helper.dropdown_max_select({ 'max-select'.to_sym => 120 }, feature_flag))
- .to eq(120)
- end
+ it 'correctly returns the max amount of reviewers or assignees to allow' do
+ max = Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+
+ expect(helper.dropdown_max_select({}))
+ .to eq(max)
+ expect(helper.dropdown_max_select({ 'max-select'.to_sym => 5 }))
+ .to eq(5)
+ expect(helper.dropdown_max_select({ 'max-select'.to_sym => max + 5 }))
+ .to eq(max)
end
end
@@ -64,43 +41,19 @@ RSpec.describe FormHelper do
describe '#reviewers_dropdown_options' do
let(:merge_request) { build(:merge_request) }
- context "with the :limit_reviewer_and_assignee_size feature flag on" do
- context "with multiple reviewers" do
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_reviewers?).and_return(true)
-
- expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
- .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
- end
- end
+ context "with multiple reviewers" do
+ it 'correctly returns the max amount of reviewers or assignees to allow' do
+ allow(helper).to receive(:merge_request_supports_multiple_reviewers?).and_return(true)
- context "with only 1 reviewer" do
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
+ .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
end
end
- context "with the :limit_reviewer_and_assignee_size feature flag off" do
- before do
- stub_feature_flags(limit_reviewer_and_assignee_size: false)
- end
-
- context "with multiple reviewers" do
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_reviewers?).and_return(true)
-
- expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
- .to eq(nil)
- end
- end
-
- context "with only 1 reviewer" do
- it 'correctly returns the max amount of reviewers or assignees to allow' do
- expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ context "with only 1 reviewer" do
+ it 'correctly returns the max amount of reviewers or assignees to allow' do
+ expect(helper.reviewers_dropdown_options(merge_request)[:data][:'max-select'])
+ .to eq(1)
end
end
end
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 98a1f77b414..a6cfbfe86ca 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -32,17 +32,34 @@ RSpec.describe HooksHelper do
end
end
- describe '#link_to_test_hook' do
+ describe '#webhook_test_items' do
+ let(:triggers) { [:push_events, :note_events] }
+
+ it 'returns test items for disclosure' do
+ expect(helper.webhook_test_items(project_hook, triggers)).to eq([
+ {
+ href: test_hook_path(project_hook, triggers[0]),
+ text: 'Push events'
+ },
+ {
+ href: test_hook_path(project_hook, triggers[1]),
+ text: 'Comments'
+ }
+ ])
+ end
+ end
+
+ describe '#test_hook_path' do
let(:trigger) { 'push_events' }
it 'returns project namespaced link' do
- expect(helper.link_to_test_hook(project_hook, trigger))
- .to include("href=\"#{test_project_hook_path(project, project_hook, trigger: trigger)}\"")
+ expect(helper.test_hook_path(project_hook, trigger))
+ .to eq(test_project_hook_path(project, project_hook, trigger: trigger))
end
it 'returns admin namespaced link' do
- expect(helper.link_to_test_hook(system_hook, trigger))
- .to include("href=\"#{test_admin_hook_path(system_hook, trigger: trigger)}\"")
+ expect(helper.test_hook_path(system_hook, trigger))
+ .to eq(test_admin_hook_path(system_hook, trigger: trigger))
end
end
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index 29b2784412e..e2ee4f33eee 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -15,16 +15,12 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
context 'with vscode_web_ide=true and instance vars set' do
before do
stub_feature_flags(vscode_web_ide: true)
-
- 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
- expect(helper.ide_data)
- .to eq(
+ expect(helper.ide_data(project: project, branch: 'master', path: 'foo/README.md', merge_request: '7',
+fork_info: nil))
+ .to match(
'can-use-new-web-ide' => 'true',
'use-new-web-ide' => 'true',
'user-preferences-path' => profile_preferences_path,
@@ -35,6 +31,9 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
'csp-nonce' => 'test-csp-nonce',
'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
'file-path' => 'foo/README.md',
+ 'editor-font-family' => 'JetBrains Mono',
+ 'editor-font-format' => 'woff2',
+ 'editor-font-src-url' => a_string_matching(%r{jetbrains-mono/JetBrainsMono}),
'merge-request' => '7',
'fork-info' => nil
)
@@ -43,7 +42,8 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
it 'does not use new web ide if user.use_legacy_web_ide' do
allow(user).to receive(:use_legacy_web_ide).and_return(true)
- expect(helper.ide_data).to include('use-new-web-ide' => 'false')
+ expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
+fork_info: nil)).to include('use-new-web-ide' => 'false')
end
end
@@ -52,9 +52,9 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
stub_feature_flags(vscode_web_ide: false)
end
- context 'when instance vars are not set' do
+ context 'when instance vars and parameters are not set' do
it 'returns instance data in the hash as nil' do
- expect(helper.ide_data)
+ expect(helper.ide_data(project: nil, branch: nil, path: nil, merge_request: nil, fork_info: nil))
.to include(
'can-use-new-web-ide' => 'false',
'use-new-web-ide' => 'false',
@@ -73,15 +73,10 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
it 'returns instance data in the hash' do
fork_info = { ide_path: '/test/ide/path' }
- self.instance_variable_set(:@branch, 'master')
- self.instance_variable_set(:@path, 'foo/bar')
- self.instance_variable_set(:@merge_request, '1')
- self.instance_variable_set(:@fork_info, fork_info)
- self.instance_variable_set(:@project, project)
-
serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json
- expect(helper.ide_data)
+ expect(helper.ide_data(project: project, branch: 'master', path: 'foo/bar', merge_request: '1',
+fork_info: fork_info))
.to include(
'branch-name' => 'master',
'file-path' => 'foo/bar',
@@ -96,12 +91,12 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
context 'environments guidance experiment', :experiment do
before do
stub_experiments(in_product_guidance_environments_webide: :candidate)
- self.instance_variable_set(:@project, project)
end
context 'when project has no enviornments' do
it 'enables environment guidance' do
- expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
+ expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
+fork_info: nil)).to include('enable-environments-guidance' => 'true')
end
context 'and the callout has been dismissed' do
@@ -109,7 +104,8 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
callout = create(:callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
callout.update!(dismissed_at: Time.now - 1.week)
allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
- expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
+ expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
+fork_info: nil)).to include('enable-environments-guidance' => 'false')
end
end
end
@@ -118,7 +114,8 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
it 'disables environment guidance' do
create(:environment, project: project)
- expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
+ expect(helper.ide_data(project: project, branch: nil, path: nil, merge_request: nil,
+fork_info: nil)).to include('enable-environments-guidance' => 'false')
end
end
end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 48e94ec7e98..abf8b65dc1e 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -36,7 +36,8 @@ RSpec.describe InviteMembersHelper do
end
it 'provides the correct attributes' do
- expect(helper.common_invite_group_modal_data(group, GroupMember, 'false')).to include({ groups_filter: 'descendant_groups', parent_id: group.id })
+ expect(helper.common_invite_group_modal_data(group, GroupMember, 'false'))
+ .to include({ groups_filter: 'descendant_groups', parent_id: group.id })
end
end
@@ -46,7 +47,8 @@ RSpec.describe InviteMembersHelper do
end
it 'does not return filter attributes' do
- expect(helper.common_invite_group_modal_data(project.group, ProjectMember, 'true').keys).not_to include(:groups_filter, :parent_id)
+ expect(helper.common_invite_group_modal_data(project.group, ProjectMember, 'true').keys)
+ .not_to include(:groups_filter, :parent_id)
end
end
end
@@ -64,7 +66,7 @@ RSpec.describe InviteMembersHelper do
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
- context 'tasks_to_be_done' do
+ context 'with tasks_to_be_done' do
using RSpec::Parameterized::TableSyntax
subject(:output) { helper.common_invite_modal_dataset(source) }
@@ -79,9 +81,7 @@ RSpec.describe InviteMembersHelper do
{ value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
].to_json
)
- expect(output[:projects]).to eq(
- [{ id: project.id, title: project.title }].to_json
- )
+ expect(output[:projects]).to eq([{ id: project.id, title: project.title }].to_json)
expect(output[:new_project_path]).to eq(
source.is_a?(Project) ? '' : new_project_path(namespace_id: group.id)
)
@@ -93,8 +93,8 @@ RSpec.describe InviteMembersHelper do
end
end
- context 'inviting members for tasks' do
- where(:open_modal_param_present?, :logged_in?, :expected?) do
+ context 'when inviting members for tasks' do
+ where(:open_modal_param?, :logged_in?, :expected?) do
true | true | true
true | false | false
false | true | false
@@ -104,7 +104,7 @@ RSpec.describe InviteMembersHelper do
with_them do
before do
allow(helper).to receive(:current_user).and_return(developer) if logged_in?
- allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' }) if open_modal_param_present?
+ allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' }) if open_modal_param?
end
context 'when the source is a project' do
@@ -120,36 +120,6 @@ RSpec.describe InviteMembersHelper do
end
end
end
-
- context 'the invite_for_help_continuous_onboarding experiment' do
- where(:invite_for_help_continuous_onboarding?, :logged_in?, :expected?) do
- true | true | true
- true | false | false
- false | true | false
- false | false | false
- end
-
- with_them do
- before do
- allow(helper).to receive(:current_user).and_return(developer) if logged_in?
- stub_experiments(invite_for_help_continuous_onboarding: :candidate) if invite_for_help_continuous_onboarding?
- end
-
- context 'when the source is a project' do
- let_it_be(:source) { project }
-
- it_behaves_like 'including the tasks to be done attributes'
- end
-
- context 'when the source is a group' do
- let_it_be(:source) { group }
-
- let(:expected?) { false }
-
- it_behaves_like 'including the tasks to be done attributes'
- end
- end
- end
end
end
@@ -172,11 +142,9 @@ RSpec.describe InviteMembersHelper do
end
context 'when the user can not manage project members' do
- before do
+ it 'returns false' do
expect(helper).to receive(:can?).with(owner, :admin_project_member, project).and_return(false)
- end
- it 'returns false' do
expect(helper.can_invite_members_for_project?(project)).to eq false
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index f2e3e401766..1ae834c0769 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssuablesHelper do
+RSpec.describe IssuablesHelper, feature_category: :team_planning do
let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) }
@@ -98,7 +98,7 @@ RSpec.describe IssuablesHelper do
end
end
- describe '#assigned_issuables_count', feature_category: :project_management do
+ describe '#assigned_issuables_count', feature_category: :team_planning 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) } }
@@ -117,7 +117,7 @@ RSpec.describe IssuablesHelper do
end
end
- describe '#assigned_open_issues_count_text', feature_category: :project_management do
+ describe '#assigned_open_issues_count_text', feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project).tap { |p| p.add_developer(user) } }
@@ -200,6 +200,41 @@ RSpec.describe IssuablesHelper do
expect(content).not_to match('gl-emoji')
end
end
+
+ describe 'service desk reply to email address' do
+ let(:email) { 'user@example.com' }
+ let(:obfuscated_email) { 'us*****@e*****.c**' }
+ let(:service_desk_issue) { build_stubbed(:issue, project: project, author: User.support_bot, service_desk_reply_to: email) }
+
+ subject { helper.issuable_meta(service_desk_issue, project) }
+
+ context 'with anonymous user' do
+ before do
+ allow(helper).to receive(:current_user).and_return(nil)
+ end
+
+ it { is_expected.to have_content(obfuscated_email) }
+ end
+
+ context 'with signed in user' do
+ context 'when user has no role in project' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it { is_expected.to have_content(obfuscated_email) }
+ end
+
+ context 'when user has reporter role in project' do
+ before do
+ project.add_reporter(user)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it { is_expected.to have_content(email) }
+ end
+ end
+ end
end
describe '#issuables_state_counter_text' do
@@ -228,7 +263,7 @@ RSpec.describe IssuablesHelper do
allow(helper).to receive(:issuables_count_for_state).and_return(-1)
end
- it 'returns avigation without badges' do
+ it 'returns navigation without badges' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
.to eq('<span>Open</span>')
expect(helper.issuables_state_counter_text(:issues, :closed, true))
@@ -387,6 +422,32 @@ RSpec.describe IssuablesHelper do
expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data))
end
+ context 'for incident tab' do
+ let(:incident) { create(:incident) }
+ let(:params) do
+ ActionController::Parameters.new({
+ controller: "projects/incidents",
+ action: "show",
+ namespace_id: "foo",
+ project_id: "bar",
+ id: incident.iid
+ }).permit!
+ end
+
+ it 'includes incident attributes' do
+ @project = incident.project
+ allow(helper).to receive(:safe_params).and_return(params)
+
+ expected_data = {
+ issueType: 'incident',
+ hasLinkedAlerts: false,
+ canUpdateTimelineEvent: true
+ }
+
+ expect(helper.issuable_initial_data(incident)).to match(hash_including(expected_data))
+ end
+ end
+
describe '#sentryIssueIdentifier' do
let(:issue) { create(:issue, author: user) }
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 0024d6b7b4e..994a1ff4f75 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -300,7 +300,6 @@ RSpec.describe IssuesHelper do
import_csv_issues_path: '#',
initial_email: project.new_issuable_address(current_user, 'issue'),
initial_sort: current_user&.user_preference&.issues_sort,
- is_anonymous_search_disabled: 'true',
is_issue_repositioning_disabled: 'true',
is_project: 'true',
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
@@ -323,10 +322,6 @@ RSpec.describe IssuesHelper do
end
describe '#project_issues_list_data' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
context 'when user is signed in' do
it_behaves_like 'issues list data' do
let(:current_user) { double.as_null_object }
@@ -483,20 +478,8 @@ RSpec.describe IssuesHelper do
let_it_be(:banned_user) { build(:user, :banned) }
let_it_be(:hidden_issue) { build(:issue, author: banned_user) }
- context 'when `ban_user_feature_flag` feature flag is enabled' do
- it 'returns `true`' do
- expect(helper.issue_hidden?(hidden_issue)).to eq(true)
- end
- end
-
- context 'when `ban_user_feature_flag` feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it 'returns `false`' do
- expect(helper.issue_hidden?(hidden_issue)).to eq(false)
- end
+ it 'returns `true`' do
+ expect(helper.issue_hidden?(hidden_issue)).to eq(true)
end
end
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index 97e37023c2d..31aeff85c70 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -2,13 +2,15 @@
require 'spec_helper'
-RSpec.describe JiraConnectHelper do
+RSpec.describe JiraConnectHelper, feature_category: :integrations do
describe '#jira_connect_app_data' do
let_it_be(:installation) { create(:jira_connect_installation) }
let_it_be(:subscription) { create(:jira_connect_subscription) }
let(:user) { create(:user) }
let(:client_id) { '123' }
+ let(:enable_public_keys_storage_config) { false }
+ let(:enable_public_keys_storage_setting) { false }
before do
stub_application_setting(jira_connect_application_key: client_id)
@@ -19,7 +21,10 @@ RSpec.describe JiraConnectHelper do
context 'user is not logged in' do
before do
allow(view).to receive(:current_user).and_return(nil)
- allow(Gitlab).to receive_message_chain('config.gitlab.url') { 'http://test.host' }
+ allow(Gitlab.config.gitlab).to receive(:url).and_return('http://test.host')
+ allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
+ .and_return(enable_public_keys_storage_config)
+ stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage_setting)
end
it 'includes Jira Connect app attributes' do
@@ -86,19 +91,6 @@ RSpec.describe JiraConnectHelper do
oauth_token_path: '/oauth/token'
)
end
-
- context 'and jira_connect_oauth_self_managed feature is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it 'does not point urls to the self-managed instance' do
- expect(parsed_oauth_metadata).not_to include(
- oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'),
- oauth_token_path: 'https://gitlab.example.com/oauth/token'
- )
- end
- end
end
end
@@ -111,6 +103,26 @@ RSpec.describe JiraConnectHelper do
it 'assigns gitlab_user_path to nil' do
expect(subject[:gitlab_user_path]).to be_nil
end
+
+ it 'assignes public_key_storage_enabled to false' do
+ expect(subject[:public_key_storage_enabled]).to eq(false)
+ end
+
+ context 'when public_key_storage is enabled via config' do
+ let(:enable_public_keys_storage_config) { true }
+
+ it 'assignes public_key_storage_enabled to true' do
+ expect(subject[:public_key_storage_enabled]).to eq(true)
+ end
+ end
+
+ context 'when public_key_storage is enabled via setting' do
+ let(:enable_public_keys_storage_setting) { true }
+
+ it 'assignes public_key_storage_enabled to true' do
+ expect(subject[:public_key_storage_enabled]).to eq(true)
+ end
+ end
end
context 'user is logged in' do
diff --git a/spec/helpers/learn_gitlab_helper_spec.rb b/spec/helpers/learn_gitlab_helper_spec.rb
deleted file mode 100644
index 0d4f1965d92..00000000000
--- a/spec/helpers/learn_gitlab_helper_spec.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe LearnGitlabHelper do
- include AfterNextHelpers
- include Devise::Test::ControllerHelpers
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, name: Onboarding::LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
- let_it_be(:namespace) { project.namespace }
-
- before do
- allow_next_instance_of(Onboarding::LearnGitlab) do |learn_gitlab|
- allow(learn_gitlab).to receive(:project).and_return(project)
- end
-
- Onboarding::Progress.onboard(namespace)
- Onboarding::Progress.register(namespace, :git_write)
- end
-
- describe '#learn_gitlab_enabled?' do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, namespace: user.namespace) }
-
- let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
-
- subject { helper.learn_gitlab_enabled?(project) }
-
- where(:onboarding, :learn_gitlab_available, :result) do
- true | true | true
- true | false | false
- false | true | false
- end
-
- with_them do
- before do
- allow(Onboarding::Progress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
- allow_next(Onboarding::LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
- end
-
- context 'when signed in' do
- before do
- sign_in(user)
- end
-
- it { is_expected.to eq(result) }
- end
- end
-
- context 'when not signed in' do
- it { is_expected.to eq(false) }
- end
- end
-
- describe '#learn_gitlab_data' do
- subject(:learn_gitlab_data) { helper.learn_gitlab_data(project) }
-
- let(:onboarding_actions_data) { Gitlab::Json.parse(learn_gitlab_data[:actions]).deep_symbolize_keys }
- let(:onboarding_sections_data) { Gitlab::Json.parse(learn_gitlab_data[:sections]).deep_symbolize_keys }
- let(:onboarding_project_data) { Gitlab::Json.parse(learn_gitlab_data[:project]).deep_symbolize_keys }
-
- shared_examples 'has all data' do
- it 'has all actions' do
- expected_keys = [
- :issue_created,
- :git_write,
- :pipeline_created,
- :merge_request_created,
- :user_added,
- :trial_started,
- :required_mr_approvals_enabled,
- :code_owners_enabled,
- :security_scan_enabled
- ]
-
- expect(onboarding_actions_data.keys).to contain_exactly(*expected_keys)
- end
-
- it 'has all section data', :aggregate_failures do
- expect(onboarding_sections_data.keys).to contain_exactly(:deploy, :plan, :workspace)
- expect(onboarding_sections_data.values.map(&:keys)).to match_array([[:svg]] * 3)
- end
-
- it 'has all project data', :aggregate_failures do
- expect(onboarding_project_data.keys).to contain_exactly(:name)
- expect(onboarding_project_data.values).to match_array([project.name])
- end
- end
-
- it_behaves_like 'has all data'
-
- it 'sets correct completion statuses' do
- expect(onboarding_actions_data).to match({
- issue_created: a_hash_including(completed: false),
- git_write: a_hash_including(completed: true),
- pipeline_created: a_hash_including(completed: false),
- merge_request_created: a_hash_including(completed: false),
- user_added: a_hash_including(completed: false),
- trial_started: a_hash_including(completed: false),
- required_mr_approvals_enabled: a_hash_including(completed: false),
- code_owners_enabled: a_hash_including(completed: false),
- security_scan_enabled: a_hash_including(completed: false)
- })
- end
-
- describe 'security_actions_continuous_onboarding experiment' do
- let(:base_paths) do
- {
- trial_started: a_hash_including(url: %r{/learn_gitlab/-/issues/2\z}),
- pipeline_created: a_hash_including(url: %r{/learn_gitlab/-/issues/7\z}),
- code_owners_enabled: a_hash_including(url: %r{/learn_gitlab/-/issues/10\z}),
- required_mr_approvals_enabled: a_hash_including(url: %r{/learn_gitlab/-/issues/11\z}),
- issue_created: a_hash_including(url: %r{/learn_gitlab/-/issues\z}),
- git_write: a_hash_including(url: %r{/learn_gitlab\z}),
- user_added: a_hash_including(url: %r{/learn_gitlab/-/project_members\z}),
- merge_request_created: a_hash_including(url: %r{/learn_gitlab/-/merge_requests\z})
- }
- end
-
- context 'when control' do
- before do
- stub_experiments(security_actions_continuous_onboarding: :control)
- end
-
- it 'sets correct paths' do
- expect(onboarding_actions_data).to match(
- base_paths.merge(
- security_scan_enabled: a_hash_including(
- url: %r{/learn_gitlab/-/security/configuration\z}
- )
- )
- )
- end
- end
-
- context 'when candidate' do
- before do
- stub_experiments(security_actions_continuous_onboarding: :candidate)
- end
-
- it 'sets correct paths' do
- expect(onboarding_actions_data).to match(
- base_paths.merge(
- license_scanning_run: a_hash_including(
- url: described_class::LICENSE_SCANNING_RUN_URL
- ),
- secure_dependency_scanning_run: a_hash_including(
- url: project_security_configuration_path(project, anchor: 'dependency-scanning')
- ),
- secure_dast_run: a_hash_including(
- url: project_security_configuration_path(project, anchor: 'dast')
- )
- )
- )
- end
- end
- end
- end
-end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index fb23b5c1dc8..6b43e97a0b4 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestsHelper do
+RSpec.describe MergeRequestsHelper, feature_category: :code_review_workflow do
include ProjectForksHelper
describe '#format_mr_branch_names' do
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index f7500709d0e..3e6780d6831 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -45,118 +45,6 @@ RSpec.describe NamespacesHelper do
user_group.add_owner(user)
end
- describe '#namespaces_options' do
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'returns groups without being a member for admin' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id)
-
- expect(options).to include(admin_group.name)
- expect(options).to include(user_group.name)
- end
- end
-
- context 'when admin mode is disabled' do
- it 'returns only allowed namespaces for admin' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: user_group.id)
-
- expect(options).to include(admin_group.name)
- expect(options).not_to include(user_group.name)
- end
- end
-
- it 'returns only allowed namespaces for user' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options
-
- expect(options).not_to include(admin_group.name)
- expect(options).to include(user_group.name)
- expect(options).to include(user.name)
- end
-
- it 'avoids duplicate groups when extra_group is used' do
- allow(helper).to receive(:current_user).and_return(admin)
-
- options = helper.namespaces_options(user_group.id, display_path: true, extra_group: build(:group, name: admin_group.name))
-
- expect(options.scan("data-name=\"#{admin_group.name}\"").count).to eq(1)
- expect(options).to include(admin_group.name)
- end
-
- context 'when admin mode is disabled' do
- it 'selects existing group' do
- allow(helper).to receive(:current_user).and_return(admin)
- user_group.add_owner(admin)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: user_group)
-
- expect(options).to include("selected=\"selected\" value=\"#{user_group.id}\"")
- expect(options).to include(admin_group.name)
- end
- end
-
- it 'selects the new group by default' do
- # Ensure we don't select a group with the same name
- create(:group, name: 'new-group', path: 'another-path')
-
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: 'new-group', path: 'new-group'))
-
- expect(options).to include(user_group.name)
- expect(options).not_to include(admin_group.name)
- expect(options).to include("selected=\"selected\" value=\"-1\"")
- end
-
- it 'falls back to current user selection' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(:extra_group, display_path: true, extra_group: build(:group, name: admin_group.name))
-
- expect(options).to include(user_group.name)
- expect(options).not_to include(admin_group.name)
- expect(options).to include("selected=\"selected\" value=\"#{user.namespace.id}\"")
- end
-
- it 'returns only groups if groups_only option is true' do
- allow(helper).to receive(:current_user).and_return(user)
-
- options = helper.namespaces_options(nil, groups_only: true)
-
- expect(options).not_to include(user.name)
- expect(options).to include(user_group.name)
- end
-
- context 'when nested groups are available' do
- it 'includes groups nested in groups the user can administer' do
- allow(helper).to receive(:current_user).and_return(user)
- child_group = create(:group, :private, parent: user_group)
-
- options = helper.namespaces_options
-
- expect(options).to include(child_group.name)
- end
-
- it 'orders the groups correctly' do
- allow(helper).to receive(:current_user).and_return(user)
- child_group = create(:group, :private, parent: user_group)
- other_child = create(:group, :private, parent: user_group)
- sub_child = create(:group, :private, parent: child_group)
-
- expect(helper).to receive(:options_for_group)
- .with([user_group, child_group, sub_child, other_child], anything)
- .and_call_original
- allow(helper).to receive(:options_for_group).and_call_original
-
- helper.namespaces_options
- end
- end
- end
-
describe '#cascading_namespace_settings_popover_data' do
attribute = :delayed_project_removal
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 3a65131aab0..3a66fe474ab 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -2,41 +2,29 @@
require 'spec_helper'
-RSpec.describe Nav::NewDropdownHelper do
+RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
describe '#new_dropdown_view_model' do
- let_it_be(:user) { build_stubbed(:user) }
-
+ let(:user) { build_stubbed(:user) }
let(:current_user) { user }
let(:current_project) { nil }
let(:current_group) { nil }
-
let(:with_can_create_project) { false }
let(:with_can_create_group) { false }
let(:with_can_create_snippet) { false }
- let(:subject) { helper.new_dropdown_view_model(project: current_project, group: current_group) }
-
- def expected_menu_section(title:, menu_item:)
- [
- {
- title: title,
- menu_items: [menu_item]
- }
- ]
- end
+ subject(:view_model) { helper.new_dropdown_view_model(project: current_project, group: current_group) }
before do
allow(helper).to receive(:current_user) { current_user }
- allow(helper).to receive(:can?) { false }
-
+ allow(helper).to receive(:can?).and_return(false)
allow(user).to receive(:can_create_project?) { with_can_create_project }
allow(user).to receive(:can_create_group?) { with_can_create_group }
allow(user).to receive(:can?).with(:create_snippet) { with_can_create_snippet }
end
- shared_examples 'invite member link shared example' do
+ shared_examples 'invite member item' do
it 'shows invite member link with emoji' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
title: expected_title,
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
@@ -46,7 +34,8 @@ RSpec.describe Nav::NewDropdownHelper do
href: expected_href,
data: {
track_action: 'click_link_invite_members',
- track_label: 'plus_menu_dropdown'
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
}
)
)
@@ -55,34 +44,37 @@ RSpec.describe Nav::NewDropdownHelper do
end
it 'has title' do
- expect(subject[:title]).to eq('Create new...')
+ expect(view_model[:title]).to eq('Create new...')
end
context 'when current_user is nil (anonymous)' do
let(:current_user) { nil }
- it 'is nil' do
- expect(subject).to be_nil
- end
+ it { is_expected.to be_nil }
end
context 'when group and project are nil' do
it 'has no menu sections' do
- expect(subject[:menu_sections]).to eq([])
+ expect(view_model[:menu_sections]).to eq([])
end
context 'when can create project' do
let(:with_can_create_project) { true }
it 'has project menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: _('GitLab'),
+ title: _('In GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_project',
title: 'New project/repository',
href: '/projects/new',
- data: { track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', qa_selector: 'global_new_project_link' }
+ data: {
+ track_action: 'click_link_new_project',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ qa_selector: 'global_new_project_link'
+ }
)
)
)
@@ -93,14 +85,19 @@ RSpec.describe Nav::NewDropdownHelper do
let(:with_can_create_group) { true }
it 'has group menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: _('GitLab'),
+ title: _('In GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_group',
title: 'New group',
href: '/groups/new',
- data: { qa_selector: 'global_new_group_link', track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
+ data: {
+ track_action: 'click_link_new_group',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ qa_selector: 'global_new_group_link'
+ }
)
)
)
@@ -111,14 +108,19 @@ RSpec.describe Nav::NewDropdownHelper do
let(:with_can_create_snippet) { true }
it 'has new snippet menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: _('GitLab'),
+ title: _('In GitLab'),
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'general_new_snippet',
title: 'New snippet',
href: '/-/snippets/new',
- data: { track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', qa_selector: 'global_new_snippet_link' }
+ data: {
+ track_action: 'click_link_new_snippet_parent',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ qa_selector: 'global_new_snippet_link'
+ }
)
)
)
@@ -127,36 +129,42 @@ RSpec.describe Nav::NewDropdownHelper do
end
context 'with persisted group' do
- let_it_be(:group) { build_stubbed(:group) }
-
+ let(:group) { build_stubbed(:group) }
let(:current_group) { group }
let(:with_can_create_projects_in_group) { false }
let(:with_can_create_subgroup_in_group) { false }
let(:with_can_admin_in_group) { false }
before do
- allow(group).to receive(:persisted?) { true }
- allow(helper).to receive(:can?).with(current_user, :create_projects, group) { with_can_create_projects_in_group }
- allow(helper).to receive(:can?).with(current_user, :create_subgroup, group) { with_can_create_subgroup_in_group }
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, group) { with_can_admin_in_group }
+ allow(group).to receive(:persisted?).and_return(true)
+ allow(helper)
+ .to receive(:can?).with(current_user, :create_projects, group) { with_can_create_projects_in_group }
+ allow(helper)
+ .to receive(:can?).with(current_user, :create_subgroup, group) { with_can_create_subgroup_in_group }
+ allow(helper)
+ .to receive(:can?).with(current_user, :admin_group_member, group) { with_can_admin_in_group }
end
it 'has no menu sections' do
- expect(subject[:menu_sections]).to eq([])
+ expect(view_model[:menu_sections]).to eq([])
end
context 'when can create projects in group' do
let(:with_can_create_projects_in_group) { true }
it 'has new project menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: 'This group',
+ title: 'In this group',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'new_project',
title: 'New project/repository',
href: "/projects/new?namespace_id=#{group.id}",
- data: { track_action: 'click_link_new_project_group', track_label: 'plus_menu_dropdown' }
+ data: {
+ track_action: 'click_link_new_project_group',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
+ }
)
)
)
@@ -167,14 +175,18 @@ RSpec.describe Nav::NewDropdownHelper do
let(:with_can_create_subgroup_in_group) { true }
it 'has new subgroup menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: 'This group',
+ title: 'In this group',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'new_subgroup',
title: 'New subgroup',
href: "/groups/new?parent_id=#{group.id}#create-group-pane",
- data: { track_action: 'click_link_new_subgroup', track_label: 'plus_menu_dropdown' }
+ data: {
+ track_action: 'click_link_new_subgroup',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
+ }
)
)
)
@@ -184,17 +196,16 @@ RSpec.describe Nav::NewDropdownHelper do
context 'when can invite members' do
let(:with_can_admin_in_group) { true }
let(:with_invite_members_experiment) { true }
- let(:expected_title) { 'This group' }
+ let(:expected_title) { 'In this group' }
let(:expected_href) { "/groups/#{group.full_path}/-/group_members" }
- it_behaves_like 'invite member link shared example'
+ it_behaves_like 'invite member item'
end
end
context 'with persisted project' do
- let_it_be(:project) { build_stubbed(:project) }
- let_it_be(:merge_project) { build_stubbed(:project) }
-
+ let(:project) { build_stubbed(:project) }
+ let(:merge_project) { build_stubbed(:project) }
let(:current_project) { project }
let(:with_show_new_issue_link) { false }
let(:with_merge_project) { nil }
@@ -209,21 +220,26 @@ RSpec.describe Nav::NewDropdownHelper do
end
it 'has no menu sections' do
- expect(subject[:menu_sections]).to eq([])
+ expect(view_model[:menu_sections]).to eq([])
end
context 'with show_new_issue_link?' do
let(:with_show_new_issue_link) { true }
it 'shows new issue menu item' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: 'This project',
+ title: 'In this project',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'new_issue',
title: 'New issue',
href: "/#{project.path_with_namespace}/-/issues/new",
- data: { track_action: 'click_link_new_issue', track_label: 'plus_menu_dropdown', qa_selector: 'new_issue_link' }
+ data: {
+ track_action: 'click_link_new_issue',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top',
+ qa_selector: 'new_issue_link'
+ }
)
)
)
@@ -234,14 +250,18 @@ RSpec.describe Nav::NewDropdownHelper do
let(:with_merge_project) { merge_project }
it 'shows merge project' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: 'This project',
+ title: 'In this project',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'new_mr',
title: 'New merge request',
href: "/#{merge_project.path_with_namespace}/-/merge_requests/new",
- data: { track_action: 'click_link_new_mr', track_label: 'plus_menu_dropdown' }
+ data: {
+ track_action: 'click_link_new_mr',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
+ }
)
)
)
@@ -252,14 +272,18 @@ RSpec.describe Nav::NewDropdownHelper do
let(:with_can_create_snippet_in_project) { true }
it 'shows new snippet' do
- expect(subject[:menu_sections]).to eq(
+ expect(view_model[:menu_sections]).to eq(
expected_menu_section(
- title: 'This project',
+ title: 'In this project',
menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
id: 'new_snippet',
title: 'New snippet',
href: "/#{project.path_with_namespace}/-/snippets/new",
- data: { track_action: 'click_link_new_snippet_project', track_label: 'plus_menu_dropdown' }
+ data: {
+ track_action: 'click_link_new_snippet_project',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
+ }
)
)
)
@@ -269,11 +293,50 @@ RSpec.describe Nav::NewDropdownHelper do
context 'when invite members experiment' do
let(:with_invite_members_experiment) { true }
let(:with_can_admin_project_member) { true }
- let(:expected_title) { 'This project' }
+ let(:expected_title) { 'In this project' }
let(:expected_href) { "/#{project.path_with_namespace}/-/project_members" }
- it_behaves_like 'invite member link shared example'
+ it_behaves_like 'invite member item'
+ end
+ end
+
+ context 'with persisted group and project' do
+ let(:project) { build_stubbed(:project) }
+ let(:group) { build_stubbed(:group) }
+ let(:current_project) { project }
+ let(:current_group) { group }
+
+ before do
+ allow(helper).to receive(:show_new_issue_link?).with(project).and_return(true)
+ allow(helper).to receive(:can?).with(current_user, :create_projects, group).and_return(true)
+ end
+
+ it 'gives precedence to group over project' do
+ group_section = expected_menu_section(
+ title: 'In this group',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'new_project',
+ title: 'New project/repository',
+ href: "/projects/new?namespace_id=#{group.id}",
+ data: {
+ track_action: 'click_link_new_project_group',
+ track_label: 'plus_menu_dropdown',
+ track_property: 'navigation_top'
+ }
+ )
+ )
+
+ expect(view_model[:menu_sections]).to eq(group_section)
end
end
+
+ def expected_menu_section(title:, menu_item:)
+ [
+ {
+ title: title,
+ menu_items: [menu_item]
+ }
+ ]
+ end
end
end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index c4a8536032e..ce5ac2e5404 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -125,6 +125,7 @@ RSpec.describe Nav::TopNavHelper do
data: {
track_action: 'click_dropdown',
track_label: 'projects_dropdown',
+ track_property: 'navigation_top',
qa_selector: 'projects_dropdown'
},
icon: 'project',
@@ -222,6 +223,7 @@ RSpec.describe Nav::TopNavHelper do
data: {
track_action: 'click_dropdown',
track_label: 'groups_dropdown',
+ track_property: 'navigation_top',
qa_selector: 'groups_dropdown'
},
icon: 'group',
@@ -517,7 +519,7 @@ RSpec.describe Nav::TopNavHelper do
{
track_label: "menu_#{label}",
track_action: 'click_dropdown',
- track_property: 'navigation'
+ track_property: 'navigation_top'
}
end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index 34d7cadf048..eb42ce18da0 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -257,12 +257,17 @@ RSpec.describe PageLayoutHelper do
let(:time) { 3.hours.ago }
before do
- user.status = UserStatus.new(message: 'Some message', emoji: 'basketball', availability: 'busy', clear_status_at: time)
+ user.status = UserStatus.new(
+ message: 'Some message',
+ emoji: 'basketball',
+ availability: 'busy',
+ clear_status_at: time
+ )
end
it 'merges the status properties with the defaults' do
is_expected.to eq({
- current_clear_status_after: time.to_s,
+ current_clear_status_after: time.to_s(:iso8601),
current_availability: 'busy',
current_emoji: 'basketball',
current_message: 'Some message',
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 898999e328e..9d1564dfef1 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe PreferencesHelper do
expect(helper.dashboard_choices).to match_array [
{ text: "Your Projects (default)", value: 'projects' },
{ text: "Starred Projects", value: 'stars' },
+ { text: "Your Activity", value: 'your_activity' },
{ text: "Your Projects' Activity", value: 'project_activity' },
{ text: "Starred Projects' Activity", value: 'starred_project_activity' },
{ text: "Followed Users' Activity", value: 'followed_user_activity' },
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
index 2b70201456a..8ef81c49fa7 100644
--- a/spec/helpers/projects/ml/experiments_helper_spec.rb
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -77,10 +77,10 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
end
end
- describe '#candidate_as_data' do
+ describe '#show_candidate_view_model' do
let(:candidate) { candidate0 }
- subject { Gitlab::Json.parse(helper.candidate_as_data(candidate)) }
+ subject { Gitlab::Json.parse(helper.show_candidate_view_model(candidate))['candidate'] }
it 'generates the correct params' do
expect(subject['params']).to include(
@@ -109,4 +109,68 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
expect(subject['info']).to include(expected_info)
end
end
+
+ describe '#experiments_as_data' do
+ let(:experiments) { [experiment] }
+
+ subject { Gitlab::Json.parse(helper.experiments_as_data(project, experiments)) }
+
+ before do
+ allow(experiment).to receive(:candidate_count).and_return(2)
+ end
+
+ it 'generates the correct info' do
+ expected_info = {
+ "name" => experiment.name,
+ "path" => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}",
+ "candidate_count" => 2
+ }
+
+ expect(subject[0]).to eq(expected_info)
+ end
+ end
+
+ describe '#page_info' do
+ def paginator(cursor = nil)
+ experiment.candidates.keyset_paginate(cursor: cursor, per_page: 1)
+ end
+
+ let_it_be(:first_page) { paginator }
+ let_it_be(:second_page) { paginator(first_page.cursor_for_next_page) }
+
+ let(:page) { nil }
+
+ subject { helper.page_info(page) }
+
+ context 'when is first page' do
+ let(:page) { first_page }
+
+ it 'generates the correct page_info' do
+ is_expected.to include({
+ has_next_page: true,
+ has_previous_page: false,
+ start_cursor: nil
+ })
+ end
+ end
+
+ context 'when is last page' do
+ let(:page) { second_page }
+
+ it 'generates the correct page_info' do
+ is_expected.to include({
+ has_next_page: false,
+ has_previous_page: true,
+ start_cursor: second_page.cursor_for_previous_page,
+ end_cursor: nil
+ })
+ end
+ end
+ end
+
+ describe '#formatted_page_info' do
+ it 'formats to json' do
+ expect(helper.formatted_page_info({ a: 1, b: 'c' })).to eq("{\"a\":1,\"b\":\"c\"}")
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 91dd4c46a74..477b5cd7753 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectsHelper do
+RSpec.describe ProjectsHelper, feature_category: :source_code_management do
include ProjectForksHelper
include AfterNextHelpers
@@ -582,46 +582,24 @@ RSpec.describe ProjectsHelper do
end
end
- describe '#show_merge_request_count' do
+ describe '#show_count?' do
context 'enabled flag' do
it 'returns true if compact mode is disabled' do
- expect(helper.show_merge_request_count?).to be_truthy
+ expect(helper.show_count?).to be_truthy
end
it 'returns false if compact mode is enabled' do
- expect(helper.show_merge_request_count?(compact_mode: true)).to be_falsey
+ expect(helper.show_count?(compact_mode: true)).to be_falsey
end
end
context 'disabled flag' do
it 'returns false if disabled flag is true' do
- expect(helper.show_merge_request_count?(disabled: true)).to be_falsey
+ expect(helper.show_count?(disabled: true)).to be_falsey
end
it 'returns true if disabled flag is false' do
- expect(helper.show_merge_request_count?).to be_truthy
- end
- end
- end
-
- describe '#show_issue_count?' do
- context 'enabled flag' do
- it 'returns true if compact mode is disabled' do
- expect(helper.show_issue_count?).to be_truthy
- end
-
- it 'returns false if compact mode is enabled' do
- expect(helper.show_issue_count?(compact_mode: true)).to be_falsey
- end
- end
-
- context 'disabled flag' do
- it 'returns false if disabled flag is true' do
- expect(helper.show_issue_count?(disabled: true)).to be_falsey
- end
-
- it 'returns true if disabled flag is false' do
- expect(helper.show_issue_count?).to be_truthy
+ expect(helper).to be_show_count
end
end
end
@@ -1050,6 +1028,28 @@ RSpec.describe ProjectsHelper do
end
end
end
+
+ describe '#able_to_see_forks_count?' do
+ subject { helper.able_to_see_forks_count?(project, user) }
+
+ where(:can_read_code, :forking_enabled, :expected) do
+ false | false | false
+ true | false | false
+ false | true | false
+ true | true | true
+ end
+
+ with_them do
+ before do
+ allow(project).to receive(:forking_enabled?).and_return(forking_enabled)
+ allow(helper).to receive(:can?).with(user, :read_code, project).and_return(can_read_code)
+ end
+
+ it 'returns the correct response' do
+ expect(subject).to eq(expected)
+ end
+ end
+ end
end
describe '#fork_button_disabled_tooltip' do
@@ -1352,4 +1352,30 @@ RSpec.describe ProjectsHelper do
end
end
end
+
+ describe '#vue_fork_divergence_data' do
+ it 'returns empty hash when fork source is not available' do
+ expect(helper.vue_fork_divergence_data(project, 'ref')).to eq({})
+ end
+
+ context 'when fork source is available' do
+ it 'returns the data related to fork divergence' do
+ source_project = project_with_repo
+
+ allow(helper).to receive(:visible_fork_source).with(project).and_return(source_project)
+
+ ahead_path =
+ "/#{project.full_path}/-/compare/#{source_project.default_branch}...ref?from_project_id=#{source_project.id}"
+ behind_path =
+ "/#{source_project.full_path}/-/compare/ref...#{source_project.default_branch}?from_project_id=#{project.id}"
+
+ expect(helper.vue_fork_divergence_data(project, 'ref')).to eq({
+ source_name: source_project.full_name,
+ source_path: project_path(source_project),
+ ahead_compare_path: ahead_path,
+ behind_compare_path: behind_path
+ })
+ end
+ end
+ end
end
diff --git a/spec/helpers/registrations_helper_spec.rb b/spec/helpers/registrations_helper_spec.rb
index b2f9a794cb3..eec87bc8712 100644
--- a/spec/helpers/registrations_helper_spec.rb
+++ b/spec/helpers/registrations_helper_spec.rb
@@ -8,4 +8,20 @@ RSpec.describe RegistrationsHelper do
expect(helper.signup_username_data_attributes.keys).to include(:min_length, :min_length_message, :max_length, :max_length_message, :qa_selector)
end
end
+
+ describe '#arkose_labs_challenge_enabled?' do
+ before do
+ stub_application_setting(
+ arkose_labs_private_api_key: nil,
+ arkose_labs_public_api_key: nil,
+ arkose_labs_namespace: nil
+ )
+ stub_env('ARKOSE_LABS_PRIVATE_KEY', nil)
+ stub_env('ARKOSE_LABS_PUBLIC_KEY', nil)
+ end
+
+ it 'is false' do
+ expect(helper.arkose_labs_challenge_enabled?).to eq false
+ end
+ end
end
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 299e4cb0133..672c2ef7589 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe SidebarsHelper do
+RSpec.describe SidebarsHelper, feature_category: :navigation do
include Devise::Test::ControllerHelpers
describe '#sidebar_tracking_attributes_by_object' do
- subject { helper.sidebar_tracking_attributes_by_object(object) }
+ subject(:tracking_attrs) { helper.sidebar_tracking_attributes_by_object(object) }
before do
stub_application_setting(snowplow_enabled: true)
@@ -16,7 +16,13 @@ RSpec.describe SidebarsHelper do
let(:object) { build(:project) }
it 'returns tracking attrs for project' do
- expect(subject[:data]).to eq({ track_label: 'projects_side_navigation', track_property: 'projects_side_navigation', track_action: 'render' })
+ attrs = {
+ track_label: 'projects_side_navigation',
+ track_property: 'projects_side_navigation',
+ track_action: 'render'
+ }
+
+ expect(tracking_attrs[:data]).to eq(attrs)
end
end
@@ -24,7 +30,13 @@ RSpec.describe SidebarsHelper do
let(:object) { build(:group) }
it 'returns tracking attrs for group' do
- expect(subject[:data]).to eq({ track_label: 'groups_side_navigation', track_property: 'groups_side_navigation', track_action: 'render' })
+ attrs = {
+ track_label: 'groups_side_navigation',
+ track_property: 'groups_side_navigation',
+ track_action: 'render'
+ }
+
+ expect(tracking_attrs[:data]).to eq(attrs)
end
end
@@ -32,38 +44,112 @@ RSpec.describe SidebarsHelper do
let(:object) { build(:user) }
it 'returns tracking attrs for user' do
- expect(subject[:data]).to eq({ track_label: 'user_side_navigation', track_property: 'user_side_navigation', track_action: 'render' })
+ attrs = {
+ track_label: 'user_side_navigation',
+ track_property: 'user_side_navigation',
+ track_action: 'render'
+ }
+
+ expect(tracking_attrs[:data]).to eq(attrs)
end
end
context 'when object is something else' do
let(:object) { build(:ci_pipeline) }
- it 'returns no attributes' do
- expect(subject).to eq({})
- end
+ it { is_expected.to eq({}) }
end
end
describe '#super_sidebar_context' do
let(:user) { build(:user) }
+ let(:group) { build(:group) }
- subject { helper.super_sidebar_context(user) }
+ subject { helper.super_sidebar_context(user, group: group, project: nil) }
- it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do
+ before do
+ allow(helper).to receive(:current_user) { user }
Rails.cache.write(['users', user.id, 'assigned_open_issues_count'], 1)
- Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 2)
+ Rails.cache.write(['users', user.id, 'assigned_open_merge_requests_count'], 4)
+ Rails.cache.write(['users', user.id, 'review_requested_open_merge_requests_count'], 0)
Rails.cache.write(['users', user.id, 'todos_pending_count'], 3)
+ Rails.cache.write(['users', user.id, 'total_merge_requests_count'], 4)
+ end
- expect(subject).to eq({
+ it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do
+ expect(subject).to include({
name: user.name,
username: user.username,
avatar_url: user.avatar_url,
assigned_open_issues_count: 1,
- assigned_open_merge_requests_count: 2,
todos_pending_count: 3,
- issues_dashboard_path: issues_dashboard_path(assignee_username: user.username)
+ issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
+ total_merge_requests_count: 4,
+ support_path: helper.support_url,
+ display_whats_new: helper.display_whats_new?,
+ whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count,
+ whats_new_version_digest: helper.whats_new_version_digest,
+ show_version_check: helper.show_version_check?,
+ gitlab_version: Gitlab.version_info,
+ gitlab_version_check: helper.gitlab_version_check
})
end
+
+ it 'returns "Merge requests" menu', :use_clean_rails_memory_store_caching do
+ expect(subject[:merge_request_menu]).to eq([
+ {
+ name: _('Merge requests'),
+ items: [
+ {
+ text: _('Assigned'),
+ href: merge_requests_dashboard_path(assignee_username: user.username),
+ count: 4
+ },
+ {
+ text: _('Review requests'),
+ href: merge_requests_dashboard_path(reviewer_username: user.username),
+ count: 0
+ }
+ ]
+ }
+ ])
+ end
+
+ it 'returns "Create new" menu groups without headers', :use_clean_rails_memory_store_caching do
+ expect(subject[:create_new_menu_groups]).to eq([
+ {
+ name: "",
+ items: [
+ { href: "/projects/new", text: "New project/repository" },
+ { href: "/groups/new", text: "New group" },
+ { href: "/-/snippets/new", text: "New snippet" }
+ ]
+ }
+ ])
+ end
+
+ it 'returns "Create new" menu groups with headers', :use_clean_rails_memory_store_caching do
+ allow(group).to receive(:persisted?).and_return(true)
+ allow(helper).to receive(:can?).and_return(true)
+
+ expect(subject[:create_new_menu_groups]).to contain_exactly(
+ a_hash_including(
+ name: "In this group",
+ items: array_including(
+ { href: "/projects/new", text: "New project/repository" },
+ { href: "/groups/new#create-group-pane", text: "New subgroup" },
+ { href: "/groups/#{group.full_path}/-/group_members", text: "Invite members" }
+ )
+ ),
+ a_hash_including(
+ name: "In GitLab",
+ items: array_including(
+ { href: "/projects/new", text: "New project/repository" },
+ { href: "/groups/new", text: "New group" },
+ { href: "/-/snippets/new", text: "New snippet" }
+ )
+ )
+ )
+ end
end
end
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index 48f67a291c4..43e663464c8 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -80,10 +80,25 @@ RSpec.describe SnippetsHelper do
context 'for Project Snippets' do
let(:snippet) { public_project_snippet }
+ let(:project) { snippet.project }
it 'returns copy button of embedded snippets' do
expect(subject).to eq(copy_button(blob.id.to_s))
end
+
+ describe 'path helpers' do
+ specify '#toggle_award_emoji_project_project_snippet_path' do
+ expect(toggle_award_emoji_project_project_snippet_path(project, snippet, a: 1)).to eq(
+ "/#{project.full_path}/-/snippets/#{snippet.id}/toggle_award_emoji?a=1"
+ )
+ end
+
+ specify '#toggle_award_emoji_project_project_snippet_url' do
+ expect(toggle_award_emoji_project_project_snippet_url(project, snippet, a: 1)).to eq(
+ "http://test.host/#{project.full_path}/-/snippets/#{snippet.id}/toggle_award_emoji?a=1"
+ )
+ end
+ end
end
def copy_button(blob_id)
diff --git a/spec/helpers/timeboxes_routing_helper_spec.rb b/spec/helpers/timeboxes_routing_helper_spec.rb
deleted file mode 100644
index 952194b6704..00000000000
--- a/spec/helpers/timeboxes_routing_helper_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe TimeboxesRoutingHelper do
- let(:project) { build_stubbed(:project) }
- let(:group) { build_stubbed(:group) }
-
- describe '#milestone_path' do
- context 'for a group milestone' do
- let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
-
- it 'links to the group milestone page' do
- expect(milestone_path(milestone))
- .to eq(group_milestone_path(group, milestone))
- end
- end
-
- context 'for a project milestone' do
- let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
-
- it 'links to the project milestone page' do
- expect(milestone_path(milestone))
- .to eq(project_milestone_path(project, milestone))
- end
- end
- end
-
- describe '#milestone_url' do
- context 'for a group milestone' do
- let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
-
- it 'links to the group milestone page' do
- expect(milestone_url(milestone))
- .to eq(group_milestone_url(group, milestone))
- end
- end
-
- context 'for a project milestone' do
- let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
-
- it 'links to the project milestone page' do
- expect(milestone_url(milestone))
- .to eq(project_milestone_url(project, milestone))
- end
- end
- end
-end
diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb
index a43a73edd53..4cb179e4f60 100644
--- a/spec/helpers/users/callouts_helper_spec.rb
+++ b/spec/helpers/users/callouts_helper_spec.rb
@@ -61,15 +61,6 @@ RSpec.describe Users::CalloutsHelper do
end
end
- describe '.render_flash_user_callout' do
- it 'renders the flash_user_callout partial' do
- expect(helper).to receive(:render)
- .with(/flash_user_callout/, flash_type: :warning, message: 'foo', feature_name: 'bar')
-
- helper.render_flash_user_callout(:warning, 'foo', 'bar')
- end
- end
-
describe '.show_feature_flags_new_version?' do
subject { helper.show_feature_flags_new_version? }
diff --git a/spec/helpers/web_hooks/web_hooks_helper_spec.rb b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
index bcd9d2df1dc..fdd0be8095b 100644
--- a/spec/helpers/web_hooks/web_hooks_helper_spec.rb
+++ b/spec/helpers/web_hooks/web_hooks_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHooks::WebHooksHelper do
+RSpec.describe WebHooks::WebHooksHelper, :clean_gitlab_redis_shared_state, feature_category: :integrations do
let_it_be_with_reload(:project) { create(:project) }
let(:current_user) { nil }
@@ -43,20 +43,12 @@ RSpec.describe WebHooks::WebHooksHelper do
expect(helper).to be_show_project_hook_failed_callout(project: project)
end
- it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do
- helper.show_project_hook_failed_callout?(project: project)
-
- travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do
- expect do
- helper.show_project_hook_failed_callout?(project: project)
- end.not_to exceed_query_limit(0)
+ it 'stores a value' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with(anything, 'true', ex: 1.hour)
end
- travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do
- expect do
- helper.show_project_hook_failed_callout?(project: project)
- end.to exceed_query_limit(0)
- end
+ helper.show_project_hook_failed_callout?(project: project)
end
end
diff --git a/spec/initializers/00_deprecations_spec.rb b/spec/initializers/00_deprecations_spec.rb
index e52e64415af..a12d079082b 100644
--- a/spec/initializers/00_deprecations_spec.rb
+++ b/spec/initializers/00_deprecations_spec.rb
@@ -2,19 +2,165 @@
require 'spec_helper'
-RSpec.describe '00_deprecations' do
- where(:warning) do
- [
- "ActiveModel::Errors#keys is deprecated and will be removed in Rails 6.2",
- "Rendering actions with '.' in the name is deprecated:",
- "default_hash is deprecated and will be removed from Rails 6.2"
- ]
- end
-
- with_them do
- specify do
- expect { ActiveSupport::Deprecation.warn(warning) }
- .to raise_error(ActiveSupport::DeprecationException)
+RSpec.describe '00_deprecations', feature_category: :shared do
+ def setup_other_deprecations
+ Warning.process(__FILE__) { :default }
+ end
+
+ def load_initializer
+ load Rails.root.join('config/initializers/00_deprecations.rb')
+ end
+
+ let(:rails_env) { nil }
+ let(:gitlab_log_deprecations) { nil }
+
+ before do
+ stub_rails_env(rails_env) if rails_env
+ stub_env('GITLAB_LOG_DEPRECATIONS', gitlab_log_deprecations)
+
+ setup_other_deprecations
+
+ ActiveSupport::Deprecation.disallowed_warnings = nil
+ ActiveSupport::Notifications.unsubscribe('deprecation.rails')
+
+ load_initializer
+ end
+
+ around do |example|
+ Warning.clear(&example)
+ end
+
+ shared_examples 'logs to Gitlab::DeprecationJsonLogger' do |message, source|
+ it 'logs them to Gitlab::DeprecationJsonLogger' do
+ expect(Gitlab::DeprecationJsonLogger).to receive(:info).with(
+ message: match(/^#{message}/),
+ source: source
+ )
+
+ subject
+ end
+ end
+
+ shared_examples 'does not log to Gitlab::DeprecationJsonLogger' do
+ it 'does not log them to Gitlab::DeprecationJsonLogger' do
+ expect(Gitlab::DeprecationJsonLogger).not_to receive(:info)
+
+ subject
+ end
+ end
+
+ shared_examples 'logs to stderr' do |message|
+ it 'logs them to stderr' do
+ expect { subject }.to output(match(/^#{message}/)).to_stderr
+ end
+ end
+
+ shared_examples 'does not log to stderr' do
+ it 'does not log them to stderr' do
+ expect { subject }.not_to output.to_stderr
+ end
+ end
+
+ describe 'Ruby deprecations' do
+ context 'when catching deprecations through Kernel#warn' do
+ subject { warn('ABC gem is deprecated and will be removed') }
+
+ include_examples 'logs to Gitlab::DeprecationJsonLogger', 'ABC gem is deprecated and will be removed', 'ruby'
+ include_examples 'logs to stderr', 'ABC gem is deprecated and will be removed'
+
+ context 'when in production environment' do
+ let(:rails_env) { 'production' }
+
+ include_examples 'does not log to Gitlab::DeprecationJsonLogger'
+ include_examples 'logs to stderr', 'ABC gem is deprecated and will be removed'
+
+ context 'when GITLAB_LOG_DEPRECATIONS is set' do
+ let(:gitlab_log_deprecations) { '1' }
+
+ include_examples 'logs to Gitlab::DeprecationJsonLogger', 'ABC gem is deprecated and will be removed', 'ruby'
+ include_examples 'logs to stderr', 'ABC gem is deprecated and will be removed'
+ end
+ end
+ end
+
+ context 'when other messages from Kernel#warn' do
+ subject { warn('Sure is hot today') }
+
+ include_examples 'does not log to Gitlab::DeprecationJsonLogger'
+ include_examples 'logs to stderr', 'Sure is hot today'
+ end
+ end
+
+ describe 'Rails deprecations' do
+ context 'when catching deprecation warnings' do
+ subject { ActiveSupport::Deprecation.warn('ABC will be removed') }
+
+ include_examples 'logs to Gitlab::DeprecationJsonLogger', 'DEPRECATION WARNING: ABC will be removed', 'rails'
+ include_examples 'logs to stderr', 'DEPRECATION WARNING: ABC will be removed'
+
+ context 'when in production environment' do
+ let(:rails_env) { 'production' }
+
+ include_examples 'does not log to Gitlab::DeprecationJsonLogger'
+ include_examples 'does not log to stderr'
+
+ context 'when GITLAB_LOG_DEPRECATIONS is set' do
+ let(:gitlab_log_deprecations) { '1' }
+
+ include_examples 'logs to Gitlab::DeprecationJsonLogger', 'DEPRECATION WARNING: ABC will be removed', 'rails'
+ include_examples 'does not log to stderr'
+ end
+ end
+ end
+
+ context 'when catching disallowed warnings' do
+ before do
+ ActiveSupport::Deprecation.disallowed_warnings << /disallowed warning 1/
+ end
+
+ subject { ActiveSupport::Deprecation.warn('This is disallowed warning 1.') }
+
+ it 'raises ActiveSupport::DeprecationException' do
+ expect { subject }.to raise_error(ActiveSupport::DeprecationException)
+ end
+
+ context 'when in production environment' do
+ let(:rails_env) { 'production' }
+
+ it 'does not raise ActiveSupport::DeprecationException' do
+ expect { subject }.not_to raise_error
+ end
+
+ context 'when GITLAB_LOG_DEPRECATIONS is set' do
+ let(:gitlab_log_deprecations) { '1' }
+
+ it 'does not raise ActiveSupport::DeprecationException' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ describe 'configuring ActiveSupport::Deprecation.disallowed_warnings' do
+ it 'sets disallowed warnings' do
+ expect(ActiveSupport::Deprecation.disallowed_warnings).not_to be_empty
+ end
+
+ context 'when in production environment' do
+ let(:rails_env) { 'production' }
+
+ it 'does not set disallowed warnings' do
+ expect(ActiveSupport::Deprecation.disallowed_warnings).to be_empty
+ end
+
+ context 'when GITLAB_LOG_DEPRECATIONS is set' do
+ let(:gitlab_log_deprecations) { '1' }
+
+ it 'does not set disallowed warnings' do
+ expect(ActiveSupport::Deprecation.disallowed_warnings).to be_empty
+ end
+ end
+ end
end
end
end
diff --git a/spec/initializers/0_log_deprecations_spec.rb b/spec/initializers/0_log_deprecations_spec.rb
deleted file mode 100644
index d34be32f7d0..00000000000
--- a/spec/initializers/0_log_deprecations_spec.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe '0_log_deprecations' do
- def setup_other_deprecations
- Warning.process(__FILE__) { :default }
- end
-
- def load_initializer
- load Rails.root.join('config/initializers/0_log_deprecations.rb')
- end
-
- def with_deprecation_behavior
- behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = deprecation_behavior
- yield
- ensure
- ActiveSupport::Deprecation.behavior = behavior
- end
-
- let(:deprecation_behavior) { :stderr }
- let(:env_var) { '1' }
-
- before do
- stub_env('GITLAB_LOG_DEPRECATIONS', env_var)
- setup_other_deprecations
- load_initializer
- end
-
- after do
- ActiveSupport::Notifications.unsubscribe('deprecation.rails')
- end
-
- around do |example|
- with_deprecation_behavior do
- # reset state changed by initializer
- Warning.clear(&example)
- end
- end
-
- describe 'Ruby deprecations' do
- shared_examples 'deprecation logger' do
- it 'logs them to deprecation logger once and to stderr' do
- expect(Gitlab::DeprecationJsonLogger).to receive(:info).with(
- message: 'ABC gem is deprecated',
- source: 'ruby'
- )
-
- expect { subject }.to output.to_stderr
- end
- end
-
- context 'when catching deprecations through Kernel#warn' do
- subject { warn('ABC gem is deprecated') }
-
- include_examples 'deprecation logger'
-
- context 'with non-notify deprecation behavior' do
- let(:deprecation_behavior) { :silence }
-
- include_examples 'deprecation logger'
- end
-
- context 'with notify deprecation behavior' do
- let(:deprecation_behavior) { :notify }
-
- include_examples 'deprecation logger'
- end
- end
-
- describe 'other messages from Kernel#warn' do
- it 'does not log them to deprecation logger' do
- expect(Gitlab::DeprecationJsonLogger).not_to receive(:info)
-
- expect { warn('Sure is hot today') }.to output.to_stderr
- end
- end
-
- context 'when disabled via environment' do
- let(:env_var) { '0' }
-
- it 'does not log them to deprecation logger' do
- expect(Gitlab::DeprecationJsonLogger).not_to receive(:info)
-
- expect { warn('ABC gem is deprecated') }.to output.to_stderr
- end
- end
- end
-
- describe 'Rails deprecations' do
- subject { ActiveSupport::Deprecation.warn('ABC will be removed') }
-
- shared_examples 'deprecation logger' do
- it 'logs them to deprecation logger once' do
- expect(Gitlab::DeprecationJsonLogger).to receive(:info).with(
- message: match(/^DEPRECATION WARNING: ABC will be removed/),
- source: 'rails'
- )
-
- subject
- end
- end
-
- context 'with non-notify deprecation behavior' do
- let(:deprecation_behavior) { :silence }
-
- include_examples 'deprecation logger'
- end
-
- context 'with notify deprecation behavior' do
- let(:deprecation_behavior) { :notify }
-
- include_examples 'deprecation logger'
- end
-
- context 'when deprecations were silenced' do
- around do |example|
- silenced = ActiveSupport::Deprecation.silenced
- ActiveSupport::Deprecation.silenced = true
- example.run
- ActiveSupport::Deprecation.silenced = silenced
- end
-
- include_examples 'deprecation logger'
- end
-
- context 'when disabled via environment' do
- let(:env_var) { '0' }
-
- it 'does not log them to deprecation logger' do
- expect(Gitlab::DeprecationJsonLogger).not_to receive(:info)
-
- expect { ActiveSupport::Deprecation.warn('ABC will be removed') }.to output.to_stderr
- end
- end
- end
-end
diff --git a/spec/initializers/0_postgresql_types_spec.rb b/spec/initializers/0_postgresql_types_spec.rb
index 76b243033d0..99f9b76a34e 100644
--- a/spec/initializers/0_postgresql_types_spec.rb
+++ b/spec/initializers/0_postgresql_types_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'PostgreSQL registered types' do
- subject(:types) { ApplicationRecord.connection.send(:type_map).keys }
+ subject(:types) { ApplicationRecord.connection.reload_type_map.keys }
# These can be obtained via SELECT oid, typname from pg_type
it 'includes custom and standard OIDs' do
diff --git a/spec/initializers/check_forced_decomposition_spec.rb b/spec/initializers/check_forced_decomposition_spec.rb
new file mode 100644
index 00000000000..a216f078932
--- /dev/null
+++ b/spec/initializers/check_forced_decomposition_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'check_forced_decomposition initializer', feature_category: :pods do
+ subject(:check_forced_decomposition) do
+ load Rails.root.join('config/initializers/check_forced_decomposition.rb')
+ end
+
+ before do
+ stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', nil)
+ end
+
+ context 'for production env' do
+ before do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ end
+
+ context 'for single database' do
+ before do
+ skip_if_multiple_databases_are_setup
+ end
+
+ it { expect { check_forced_decomposition }.not_to raise_error }
+ end
+
+ context 'for multiple database' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:main_database_config) do
+ Rails.application.config.load_database_yaml
+ .dig('test', 'main')
+ .slice('adapter', 'encoding', 'database', 'username', 'password', 'host')
+ .symbolize_keys
+ end
+
+ let(:additional_database_config) do
+ # Use built-in postgres database
+ main_database_config.merge(database: 'postgres')
+ end
+
+ around do |example|
+ with_reestablished_active_record_base(reconnect: true) do
+ with_db_configs(test: test_config) do
+ example.run
+ end
+ end
+ end
+
+ context 'when ci and main share the same database' do
+ let(:test_config) do
+ {
+ main: main_database_config,
+ ci: additional_database_config.merge(database: main_database_config[:database])
+ }
+ end
+
+ it { expect { check_forced_decomposition }.not_to raise_error }
+
+ context 'when host is not present' do
+ let(:test_config) do
+ {
+ main: main_database_config.except(:host),
+ ci: additional_database_config.merge(database: main_database_config[:database]).except(:host)
+ }
+ end
+
+ it { expect { check_forced_decomposition }.not_to raise_error }
+ end
+ end
+
+ context 'when ci and main share the same database but different host' do
+ let(:test_config) do
+ {
+ main: main_database_config,
+ ci: additional_database_config.merge(
+ database: main_database_config[:database],
+ host: 'otherhost.localhost'
+ )
+ }
+ end
+
+ it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
+ end
+
+ context 'when ci and main are different databases' do
+ let(:test_config) do
+ {
+ main: main_database_config,
+ ci: additional_database_config
+ }
+ end
+
+ it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
+
+ context 'for GitLab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it { expect { check_forced_decomposition }.not_to raise_error }
+ end
+
+ context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is true' do
+ before do
+ stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'true')
+ end
+
+ it { expect { check_forced_decomposition }.not_to raise_error }
+ end
+
+ context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is false' do
+ before do
+ stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'false')
+ end
+
+ it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/initializers/countries_spec.rb b/spec/initializers/countries_spec.rb
new file mode 100644
index 00000000000..2b968e41322
--- /dev/null
+++ b/spec/initializers/countries_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+RSpec.describe 'countries', feature_category: :onboarding do
+ it 'configures locals to EN' do
+ expect(ISO3166.configuration.locales).to eq([:en])
+ end
+
+ it 'initialises Ukraine with custom country name' do
+ expect(ISO3166::Country['UA'].data["name"]).to be('Ukraine (except the Crimea, Donetsk, and Luhansk regions)')
+ end
+
+ it 'initialises Taiwan with custom country name' do
+ expect(ISO3166::Country['TW'].data["name"]).to be('Taiwan')
+ end
+end
diff --git a/spec/initializers/database_config_spec.rb b/spec/initializers/database_config_spec.rb
index bbb5e7b1923..f3f1f326dad 100644
--- a/spec/initializers/database_config_spec.rb
+++ b/spec/initializers/database_config_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe 'Database config initializer', :reestablished_active_record_base
context 'when ci database connection' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
let(:database_base_model) { Gitlab::Database.database_base_models[:ci] }
diff --git a/spec/initializers/google_api_client_spec.rb b/spec/initializers/google_api_client_spec.rb
index 0ed82d7debe..b3c4ac5e23b 100644
--- a/spec/initializers/google_api_client_spec.rb
+++ b/spec/initializers/google_api_client_spec.rb
@@ -26,8 +26,9 @@ RSpec.describe Google::Apis::Core::HttpCommand do # rubocop:disable RSpec/FilePa
it 'retries with max elapsed_time and retries' do
expect(Retriable).to receive(:retriable).with(
tries: Google::Apis::RequestOptions.default.retries + 1,
- max_elapsed_time: 3600,
+ max_elapsed_time: 900,
base_interval: 1,
+ max_interval: 60,
multiplier: 2,
on: described_class::RETRIABLE_ERRORS).and_call_original
allow(Retriable).to receive(:retriable).and_call_original
diff --git a/spec/initializers/load_balancing_spec.rb b/spec/initializers/load_balancing_spec.rb
index d9162acd2cd..66aaa52eef2 100644
--- a/spec/initializers/load_balancing_spec.rb
+++ b/spec/initializers/load_balancing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'load_balancing', :delete, :reestablished_active_record_base do
+RSpec.describe 'load_balancing', :delete, :reestablished_active_record_base, feature_category: :pods do
subject(:initialize_load_balancer) do
load Rails.root.join('config/initializers/load_balancing.rb')
end
diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb
index 92834c889c2..ef24da0071b 100644
--- a/spec/initializers/memory_watchdog_spec.rb
+++ b/spec/initializers/memory_watchdog_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe 'memory watchdog' do
+RSpec.describe 'memory watchdog', feature_category: :application_performance do
shared_examples 'starts configured watchdog' do |configure_monitor_method|
shared_examples 'configures and starts watchdog' do
it "correctly configures and starts watchdog", :aggregate_failures do
@@ -104,11 +104,7 @@ RSpec.describe 'memory watchdog' do
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
end
- it 'does not register life-cycle hook' do
- expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_start)
-
- run_initializer
- end
+ it_behaves_like 'starts configured watchdog', :configure_for_sidekiq
end
end
end
diff --git a/spec/lib/api/ci/helpers/runner_helpers_spec.rb b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
index d32f7e4f0be..c36c8d23e88 100644
--- a/spec/lib/api/ci/helpers/runner_helpers_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_helpers_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
context 'when runner info is present' do
let(:name) { 'runner' }
+ let(:system_id) { 's_c2d22f638c25' }
let(:version) { '1.2.3' }
let(:revision) { '10.0' }
let(:platform) { 'test' }
@@ -42,6 +43,7 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
let(:config) { { 'gpus' => 'all' } }
let(:runner_params) do
{
+ system_id: system_id,
'info' =>
{
'name' => name,
@@ -59,7 +61,10 @@ RSpec.describe API::Ci::Helpers::Runner, feature_category: :runner do
subject(:details) { runner_helper.get_runner_details_from_request }
it 'extracts the runner details', :aggregate_failures do
- expect(details.keys).to match_array(%w(name version revision platform architecture executor config ip_address))
+ expect(details.keys).to match_array(
+ %w(system_id name version revision platform architecture executor config ip_address)
+ )
+ expect(details['system_id']).to eq(system_id)
expect(details['name']).to eq(name)
expect(details['version']).to eq(version)
expect(details['revision']).to eq(revision)
diff --git a/spec/lib/api/ci/helpers/runner_spec.rb b/spec/lib/api/ci/helpers/runner_spec.rb
index 6801d16d13e..8264db8344d 100644
--- a/spec/lib/api/ci/helpers/runner_spec.rb
+++ b/spec/lib/api/ci/helpers/runner_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe API::Ci::Helpers::Runner do
allow(helper).to receive(:env).and_return({})
end
- describe '#current_job' do
+ describe '#current_job', feature_category: :continuous_integration do
let(:build) { create(:ci_build, :running) }
it 'handles sticking of a build when a build ID is specified' do
@@ -38,7 +38,7 @@ RSpec.describe API::Ci::Helpers::Runner do
end
end
- describe '#current_runner' do
+ describe '#current_runner', feature_category: :runner do
let(:runner) { create(:ci_runner, token: 'foo') }
it 'handles sticking of a runner if a token is specified' do
@@ -67,7 +67,79 @@ RSpec.describe API::Ci::Helpers::Runner do
end
end
- describe '#track_runner_authentication', :prometheus do
+ describe '#current_runner_machine', :freeze_time, feature_category: :runner_fleet do
+ let(:runner) { create(:ci_runner, token: 'foo') }
+ let(:runner_machine) { create(:ci_runner_machine, runner: runner, system_xid: 'bar', contacted_at: 1.hour.ago) }
+
+ subject(:current_runner_machine) { helper.current_runner_machine }
+
+ context 'with create_runner_machine FF enabled' do
+ before do
+ stub_feature_flags(create_runner_machine: true)
+ end
+
+ context 'when runner machine already exists' do
+ before do
+ allow(helper).to receive(:params).and_return(token: runner.token, system_id: runner_machine.system_xid)
+ end
+
+ it { is_expected.to eq(runner_machine) }
+
+ it 'does not update the contacted_at field' do
+ expect(current_runner_machine.contacted_at).to eq 1.hour.ago
+ end
+ end
+
+ context 'when runner machine cannot be found' do
+ it 'creates a new runner machine', :aggregate_failures do
+ allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
+
+ expect { current_runner_machine }.to change { Ci::RunnerMachine.count }.by(1)
+
+ expect(current_runner_machine).not_to be_nil
+ expect(current_runner_machine.system_xid).to eq('new_system_id')
+ expect(current_runner_machine.contacted_at).to eq(Time.current)
+ expect(current_runner_machine.runner).to eq(runner)
+ end
+
+ it 'creates a new <legacy> runner machine if system_id is not specified', :aggregate_failures do
+ allow(helper).to receive(:params).and_return(token: runner.token)
+
+ expect { current_runner_machine }.to change { Ci::RunnerMachine.count }.by(1)
+
+ expect(current_runner_machine).not_to be_nil
+ expect(current_runner_machine.system_xid).to eq(::API::Ci::Helpers::Runner::LEGACY_SYSTEM_XID)
+ expect(current_runner_machine.runner).to eq(runner)
+ end
+ end
+ end
+
+ context 'with create_runner_machine FF disabled' do
+ before do
+ stub_feature_flags(create_runner_machine: false)
+ end
+
+ it 'does not return runner machine if no system_id specified' do
+ allow(helper).to receive(:params).and_return(token: runner.token)
+
+ is_expected.to be_nil
+ end
+
+ context 'when runner machine can not be found' do
+ before do
+ allow(helper).to receive(:params).and_return(token: runner.token, system_id: 'new_system_id')
+ end
+
+ it 'does not create a new runner machine', :aggregate_failures do
+ expect { current_runner_machine }.not_to change { Ci::RunnerMachine.count }
+
+ expect(current_runner_machine).to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#track_runner_authentication', :prometheus, feature_category: :runner do
subject { helper.track_runner_authentication }
let(:runner) { create(:ci_runner, token: 'foo') }
diff --git a/spec/lib/api/entities/draft_note_spec.rb b/spec/lib/api/entities/draft_note_spec.rb
new file mode 100644
index 00000000000..59555319bb1
--- /dev/null
+++ b/spec/lib/api/entities/draft_note_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::DraftNote, feature_category: :code_review_workflow do
+ let_it_be(:entity) { create(:draft_note, :on_discussion) }
+ let_it_be(:json) { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(json["id"]).to eq entity.id
+ expect(json["author_id"]).to eq entity.author_id
+ expect(json["merge_request_id"]).to eq entity.merge_request_id
+ expect(json["resolve_discussion"]).to eq entity.resolve_discussion
+ expect(json["discussion_id"]).to eq entity.discussion_id
+ expect(json["note"]).to eq entity.note
+ expect(json["position"].transform_keys(&:to_sym)).to eq entity.position.to_h
+ end
+end
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb
index bb0e25d2613..33f8a806c50 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -4,11 +4,9 @@ require 'spec_helper'
RSpec.describe ::API::Entities::MergeRequestBasic do
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :public) }
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:labels) { create_list(:label, 3) }
let_it_be(:merge_requests) { create_list(:labeled_merge_request, 10, :unique_branches, labels: labels) }
-
let_it_be(:entity) { described_class.new(merge_request) }
# This mimics the behavior of the `Grape::Entity` serializer
@@ -16,7 +14,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
described_class.new(obj).presented
end
- subject { entity.as_json }
+ subject(:json) { entity.as_json }
it 'includes expected fields' do
expected_fields = %i[
@@ -57,7 +55,7 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
end
end
- context 'reviewers' do
+ describe 'reviewers' do
before do
merge_request.reviewers = [user]
end
@@ -68,4 +66,26 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
expect(result['reviewers'][0]['username']).to eq user.username
end
end
+
+ describe 'squash' do
+ subject { json[:squash] }
+
+ before do
+ merge_request.target_project.project_setting.update!(squash_option: :never)
+ merge_request.update!(squash: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ describe 'squash_on_merge' do
+ subject { json[:squash_on_merge] }
+
+ before do
+ merge_request.target_project.project_setting.update!(squash_option: :never)
+ merge_request.update!(squash: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
index db8f106c9fe..b64a1555332 100644
--- a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
+++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
+RSpec.describe API::Entities::Ml::Mlflow::RunInfo, feature_category: :mlops do
let_it_be(:candidate) { create(:ml_candidates) }
subject { described_class.new(candidate, packages_url: 'http://example.com').as_json }
diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb
index d1e5f191614..e750b82011b 100644
--- a/spec/lib/api/entities/release_spec.rb
+++ b/spec/lib/api/entities/release_spec.rb
@@ -77,4 +77,16 @@ RSpec.describe API::Entities::Release do
end
end
end
+
+ describe 'links' do
+ subject(:links) { entity.as_json['_links'] }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'includes links' do
+ expect(links.keys).to include('closed_issues_url', 'closed_merge_requests_url', 'edit_url', 'merged_merge_requests_url', 'opened_issues_url', 'opened_merge_requests_url', 'self')
+ end
+ end
end
diff --git a/spec/lib/api/entities/user_spec.rb b/spec/lib/api/entities/user_spec.rb
index 407f2894f01..3094fc748c9 100644
--- a/spec/lib/api/entities/user_spec.rb
+++ b/spec/lib/api/entities/user_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe API::Entities::User do
# UserBasic
:state, :avatar_url, :web_url,
# User
- :created_at, :bio, :location, :public_email, :skype, :linkedin, :twitter,
+ :created_at, :bio, :location, :public_email, :skype, :linkedin, :twitter, :discord,
:website_url, :organization, :job_title, :pronouns, :bot, :work_information,
:followers, :following, :is_followed, :local_time
)
diff --git a/spec/lib/api/helpers/caching_spec.rb b/spec/lib/api/helpers/caching_spec.rb
index 828af7b5f91..03025823357 100644
--- a/spec/lib/api/helpers/caching_spec.rb
+++ b/spec/lib/api/helpers/caching_spec.rb
@@ -47,12 +47,14 @@ RSpec.describe API::Helpers::Caching, :use_clean_rails_redis_caching do
context 'single object' do
let_it_be(:presentable) { create(:todo, project: project) }
+ let(:expected_cache_key_prefix) { 'API::Entities::Todo' }
it_behaves_like 'object cache helper'
end
context 'collection of objects' do
let_it_be(:presentable) { Array.new(5).map { create(:todo, project: project) } }
+ let(:expected_cache_key_prefix) { 'API::Entities::Todo' }
it_behaves_like 'collection cache helper'
end
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index de9d139a7b6..2a663d5e9b2 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -275,7 +275,6 @@ RSpec.describe API::Helpers::PackagesHelpers, feature_category: :package_registr
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' }
@@ -284,5 +283,60 @@ RSpec.describe API::Helpers::PackagesHelpers, feature_category: :package_registr
helper.track_package_event(action, scope, **args)
end
end
+
+ context 'when using deploy token and action is push package' do
+ let(:user) { create(:deploy_token, write_registry: true, projects: [project]) }
+ let(:scope) { :rubygems }
+ let(:category) { 'API::RubygemPackages' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'counts.package_events_i_package_push_package_by_deploy_token' }
+ let(:property) { 'i_package_push_package_by_deploy_token' }
+ let(:service_ping_context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.package_events_i_package_push_package_by_deploy_token').to_h]
+ end
+
+ it 'logs a snowplow event' do
+ args = { category: category, namespace: namespace, project: project }
+ helper.track_package_event('push_package', scope, **args)
+
+ expect_snowplow_event(
+ category: category,
+ action: 'push_package_by_deploy_token',
+ context: service_ping_context,
+ label: label,
+ namespace: namespace,
+ property: property,
+ project: project
+ )
+ end
+ end
+
+ context 'when guest and action is pull package' do
+ let(:user) { nil }
+ let(:scope) { :rubygems }
+ let(:category) { 'API::RubygemPackages' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'counts.package_events_i_package_pull_package_by_guest' }
+ let(:property) { 'i_package_pull_package_by_guest' }
+ let(:service_ping_context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.package_events_i_package_pull_package_by_guest').to_h]
+ end
+
+ it 'logs a snowplow event' do
+ allow(helper).to receive(:current_user).and_return(nil)
+ args = { category: category, namespace: namespace, project: project }
+ helper.track_package_event('pull_package', scope, **args)
+
+ expect_snowplow_event(
+ category: category,
+ action: 'pull_package_by_guest',
+ context: service_ping_context,
+ label: label,
+ namespace: namespace,
+ property: property,
+ project: project
+ )
+ end
+ end
end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index a0f5ee1ea95..0fcf36ca9dd 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -821,7 +821,7 @@ RSpec.describe API::Helpers, feature_category: :not_owned do
it 'redirects to a CDN-fronted URL' do
expect(helper).to receive(:redirect)
- expect(helper).to receive(:signed_head_url).and_call_original
+ expect(ObjectStorage::S3).to receive(:signed_head_url).and_call_original
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: artifact.file.model).and_call_original
subject
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index a8ee28d3714..f1f9dd38947 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -199,7 +199,7 @@ RSpec.describe Atlassian::JiraConnect::Client, feature_category: :integrations d
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 })
+ expect(subject.send(:request_body_schema, response)).to eq({ "foo" => 1, "bar" => 2 })
end
end
diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb
index ed5b34b7f8c..c70d47e4940 100644
--- a/spec/lib/backup/database_spec.rb
+++ b/spec/lib/backup/database_spec.rb
@@ -2,11 +2,23 @@
require 'spec_helper'
-RSpec.describe Backup::Database do
+RSpec.configure do |rspec|
+ rspec.expect_with :rspec do |c|
+ c.max_formatted_output_length = nil
+ end
+end
+
+RSpec.describe Backup::Database, feature_category: :backup_restore do
let(:progress) { StringIO.new }
let(:output) { progress.string }
+ let(:one_db_configured?) { Gitlab::Database.database_base_models.one? }
+ let(:database_models_for_backup) { Gitlab::Database.database_base_models_with_gitlab_shared }
+ let(:timeout_service) do
+ instance_double(Gitlab::Database::TransactionTimeoutSettings, restore_timeouts: nil, disable_timeouts: nil)
+ end
before(:all) do
+ Rake::Task.define_task(:environment)
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
@@ -14,14 +26,136 @@ RSpec.describe Backup::Database do
Rake.application.rake_require 'tasks/cache'
end
+ describe '#dump', :delete do
+ let(:backup_id) { 'some_id' }
+ let(:force) { true }
+
+ subject { described_class.new(progress, force: force) }
+
+ before do
+ database_models_for_backup.each do |database_name, base_model|
+ base_model.connection.rollback_transaction unless base_model.connection.open_transactions.zero?
+ allow(base_model.connection).to receive(:execute).and_call_original
+ end
+ end
+
+ it 'creates gzipped database dumps' do
+ Dir.mktmpdir do |dir|
+ subject.dump(dir, backup_id)
+
+ database_models_for_backup.each_key do |database_name|
+ filename = database_name == 'main' ? 'database.sql.gz' : "#{database_name}_database.sql.gz"
+ expect(File.exist?(File.join(dir, filename))).to eq(true)
+ end
+ end
+ end
+
+ it 'uses snapshots' do
+ Dir.mktmpdir do |dir|
+ base_model = Gitlab::Database.database_base_models['main']
+ expect(base_model.connection).to receive(:begin_transaction).with(
+ isolation: :repeatable_read
+ ).and_call_original
+ expect(base_model.connection).to receive(:execute).with(
+ "SELECT pg_export_snapshot() as snapshot_id;"
+ ).and_call_original
+ expect(base_model.connection).to receive(:rollback_transaction).and_call_original
+
+ subject.dump(dir, backup_id)
+ end
+ end
+
+ it 'disables transaction time out' do
+ number_of_databases = Gitlab::Database.database_base_models_with_gitlab_shared.count
+ expect(Gitlab::Database::TransactionTimeoutSettings)
+ .to receive(:new).exactly(2 * number_of_databases).times.and_return(timeout_service)
+ expect(timeout_service).to receive(:disable_timeouts).exactly(number_of_databases).times
+ expect(timeout_service).to receive(:restore_timeouts).exactly(number_of_databases).times
+
+ Dir.mktmpdir do |dir|
+ subject.dump(dir, backup_id)
+ end
+ end
+
+ describe 'pg_dump arguments' do
+ let(:snapshot_id) { 'fake_id' }
+ let(:pg_args) do
+ [
+ '--clean',
+ '--if-exists',
+ "--snapshot=#{snapshot_id}"
+ ]
+ end
+
+ let(:dumper) { double }
+ let(:destination_dir) { 'tmp' }
+
+ before do
+ allow(Backup::Dump::Postgres).to receive(:new).and_return(dumper)
+ allow(dumper).to receive(:dump).with(any_args).and_return(true)
+
+ database_models_for_backup.each do |database_name, base_model|
+ allow(base_model.connection).to receive(:execute).with(
+ "SELECT pg_export_snapshot() as snapshot_id;"
+ ).and_return(['snapshot_id' => snapshot_id])
+ end
+ end
+
+ it 'calls Backup::Dump::Postgres with correct pg_dump arguments' do
+ expect(dumper).to receive(:dump).with(anything, anything, pg_args)
+
+ subject.dump(destination_dir, backup_id)
+ end
+
+ context 'when a PostgreSQL schema is used' do
+ let(:schema) { 'gitlab' }
+ let(:additional_args) do
+ pg_args + ['-n', schema] + Gitlab::Database::EXTRA_SCHEMAS.flat_map do |schema|
+ ['-n', schema.to_s]
+ end
+ end
+
+ before do
+ allow(Gitlab.config.backup).to receive(:pg_schema).and_return(schema)
+ end
+
+ it 'calls Backup::Dump::Postgres with correct pg_dump arguments' do
+ expect(dumper).to receive(:dump).with(anything, anything, additional_args)
+
+ subject.dump(destination_dir, backup_id)
+ end
+ end
+ end
+
+ context 'when a StandardError (or descendant) is raised' do
+ before do
+ allow(FileUtils).to receive(:mkdir_p).and_raise(StandardError)
+ end
+
+ it 'restores timeouts' do
+ Dir.mktmpdir do |dir|
+ number_of_databases = Gitlab::Database.database_base_models_with_gitlab_shared.count
+ expect(Gitlab::Database::TransactionTimeoutSettings)
+ .to receive(:new).exactly(number_of_databases).times.and_return(timeout_service)
+ expect(timeout_service).to receive(:restore_timeouts).exactly(number_of_databases).times
+
+ expect { subject.dump(dir, backup_id) }.to raise_error StandardError
+ end
+ end
+ end
+ end
+
describe '#restore' do
let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1)] }
- let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
+ let(:backup_dir) { Rails.root.join("spec/fixtures/") }
let(:force) { true }
+ let(:rake_task) { instance_double(Rake::Task, invoke: true) }
- subject { described_class.new(Gitlab::Database::MAIN_DATABASE_NAME.to_sym, progress, force: force) }
+ subject { described_class.new(progress, force: force) }
before do
+ allow(Rake::Task).to receive(:[]).with(any_args).and_return(rake_task)
+
allow(subject).to receive(:pg_restore_cmd).and_return(cmd)
end
@@ -30,9 +164,14 @@ RSpec.describe Backup::Database do
it 'warns the user and waits' do
expect(subject).to receive(:sleep)
- expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
- subject.restore(data)
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke)
+ end
+
+ subject.restore(backup_dir)
expect(output).to include('Removing all tables. Press `Ctrl-C` within 5 seconds to abort')
end
@@ -43,12 +182,14 @@ RSpec.describe Backup::Database do
end
context 'with an empty .gz file' do
- let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
-
it 'returns successfully' do
- expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke)
+ end
- subject.restore(data)
+ subject.restore(backup_dir)
expect(output).to include("Restoring PostgreSQL database")
expect(output).to include("[DONE]")
@@ -57,12 +198,18 @@ RSpec.describe Backup::Database do
end
context 'with a corrupted .gz file' do
- let(:data) { Rails.root.join("spec/fixtures/big-image.png").to_s }
+ before do
+ allow(subject).to receive(:file_name).and_return("#{backup_dir}big-image.png")
+ end
it 'raises a backup error' do
- expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke)
+ end
- expect { subject.restore(data) }.to raise_error(Backup::Error)
+ expect { subject.restore(backup_dir) }.to raise_error(Backup::Error)
end
end
@@ -72,9 +219,13 @@ RSpec.describe Backup::Database do
let(:cmd) { %W[#{Gem.ruby} -e $stderr.write("#{noise}#{visible_error}")] }
it 'filters out noise from errors and has a post restore warning' do
- expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke)
+ end
- subject.restore(data)
+ subject.restore(backup_dir)
expect(output).to include("ERRORS")
expect(output).not_to include(noise)
@@ -95,9 +246,13 @@ RSpec.describe Backup::Database do
end
it 'overrides default config values' do
- expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).to receive(:invoke)
+ end
- subject.restore(data)
+ subject.restore(backup_dir)
expect(output).to include(%("PGHOST"=>"test.example.com"))
expect(output).to include(%("PGPASSWORD"=>"donotchange"))
@@ -107,22 +262,30 @@ RSpec.describe Backup::Database do
end
context 'when the source file is missing' do
- let(:main_database) { described_class.new(Gitlab::Database::MAIN_DATABASE_NAME.to_sym, progress, force: force) }
- let(:ci_database) { described_class.new(Gitlab::Database::CI_DATABASE_NAME.to_sym, progress, force: force) }
- let(:missing_file) { Rails.root.join("spec/fixtures/missing_file.tar.gz").to_s }
+ context 'for main database' do
+ before do
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with("#{backup_dir}database.sql.gz").and_return(false)
+ allow(File).to receive(:exist?).with("#{backup_dir}ci_database.sql.gz").and_return(false)
+ end
- it 'main database raises an error about missing source file' do
- expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke)
+ it 'raises an error about missing source file' do
+ if one_db_configured?
+ expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke)
+ else
+ expect(Rake::Task['gitlab:db:drop_tables:main']).not_to receive(:invoke)
+ end
- expect do
- main_database.restore(missing_file)
- end.to raise_error(Backup::Error, /Source database file does not exist/)
+ expect do
+ subject.restore('db')
+ end.to raise_error(Backup::Error, /Source database file does not exist/)
+ end
end
- it 'ci database tolerates missing source file' do
- expect(Rake::Task['gitlab:db:drop_tables']).not_to receive(:invoke)
- skip_if_multiple_databases_not_setup
- expect { ci_database.restore(missing_file) }.not_to raise_error
+ context 'for ci database' do
+ it 'ci database tolerates missing source file' do
+ expect { subject.restore(backup_dir) }.not_to raise_error
+ end
end
end
end
diff --git a/spec/lib/backup/dump/postgres_spec.rb b/spec/lib/backup/dump/postgres_spec.rb
new file mode 100644
index 00000000000..f6a68ab6db9
--- /dev/null
+++ b/spec/lib/backup/dump/postgres_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::Dump::Postgres, feature_category: :backup_restore do
+ describe '#dump' do
+ let(:pg_database) { 'gitlabhq_test' }
+ let(:destination_dir) { Dir.mktmpdir }
+ let(:db_file_name) { File.join(destination_dir, 'output.gz') }
+
+ let(:pipes) { IO.pipe }
+ let(:gzip_pid) { spawn('gzip -c -1', in: pipes[0], out: [db_file_name, 'w', 0o600]) }
+ let(:pg_dump_pid) { Process.spawn('pg_dump', *args, pg_database, out: pipes[1]) }
+ let(:args) { ['--help'] }
+
+ subject { described_class.new }
+
+ before do
+ allow(IO).to receive(:pipe).and_return(pipes)
+ end
+
+ after do
+ FileUtils.remove_entry destination_dir
+ end
+
+ it 'creates gzipped dump using supplied arguments' do
+ expect(subject).to receive(:spawn).with('gzip -c -1', in: pipes.first,
+ out: [db_file_name, 'w', 0o600]).and_return(gzip_pid)
+ expect(Process).to receive(:spawn).with('pg_dump', *args, pg_database, out: pipes[1]).and_return(pg_dump_pid)
+
+ subject.dump(pg_database, db_file_name, args)
+
+ expect(File.exist?(db_file_name)).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 992dbec73c2..02889c1535d 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Backup::Manager do
+RSpec.describe Backup::Manager, feature_category: :backup_restore do
include StubENV
let(:progress) { StringIO.new }
@@ -30,8 +30,7 @@ RSpec.describe Backup::Manager do
task: task,
enabled: enabled,
destination_path: 'my_task.tar.gz',
- human_name: 'my task',
- task_group: 'group1'
+ human_name: 'my task'
)
}
end
@@ -63,16 +62,6 @@ RSpec.describe Backup::Manager do
subject.run_create_task('my_task')
end
end
-
- describe 'task group skipped' do
- it 'informs the user' do
- stub_env('SKIP', 'group1')
-
- expect(Gitlab::BackupLogger).to receive(:info).with(message: 'Dumping my task ... [SKIPPED]')
-
- subject.run_create_task('my_task')
- end
- end
end
describe '#run_restore_task' do
diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb
index 3914aee2d4c..5647a81675e 100644
--- a/spec/lib/banzai/color_parser_spec.rb
+++ b/spec/lib/banzai/color_parser_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Banzai::ColorParser do
+RSpec.describe Banzai::ColorParser, feature_category: :team_planning do
describe '.parse' do
context 'HEX format' do
[
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index a10dd6eb3a2..35e0e20582d 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::CommitRenderer do
+RSpec.describe Banzai::CommitRenderer, feature_category: :source_code_management do
describe '.render', :clean_gitlab_redis_cache do
it 'renders a commit description and title' do
user = build(:user)
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 8748a910003..1c861068f16 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::CrossProjectReference do
+RSpec.describe Banzai::CrossProjectReference, feature_category: :team_planning do
let(:including_class) { Class.new.include(described_class).new }
let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {}, {}) }
diff --git a/spec/lib/banzai/filter/absolute_link_filter_spec.rb b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
index 0c159e8bac8..3e7678e3f9a 100644
--- a/spec/lib/banzai/filter/absolute_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/absolute_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AbsoluteLinkFilter do
+RSpec.describe Banzai::Filter::AbsoluteLinkFilter, feature_category: :team_planning do
def filter(doc, context = {})
described_class.call(doc, context)
end
diff --git a/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb b/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb
index 7af22ea7db1..3bd48bdd6f7 100644
--- a/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb
+++ b/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AsciiDocPostProcessingFilter do
+RSpec.describe Banzai::Filter::AsciiDocPostProcessingFilter, feature_category: :wiki do
include FilterSpecHelper
it "adds class for elements with data-math-style" do
diff --git a/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb
index 272b4386ec8..1a76b58bc43 100644
--- a/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AsciiDocSanitizationFilter do
+RSpec.describe Banzai::Filter::AsciiDocSanitizationFilter, feature_category: :wiki do
include FilterSpecHelper
it 'preserves footnotes refs' do
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index 81aa8d35ebc..004c70c28f1 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AssetProxyFilter do
+RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_planning do
include FilterSpecHelper
def image(path)
diff --git a/spec/lib/banzai/filter/attributes_filter_spec.rb b/spec/lib/banzai/filter/attributes_filter_spec.rb
index cef5e24cdaa..72d78960474 100644
--- a/spec/lib/banzai/filter/attributes_filter_spec.rb
+++ b/spec/lib/banzai/filter/attributes_filter_spec.rb
@@ -66,6 +66,8 @@ RSpec.describe Banzai::Filter::AttributesFilter, feature_category: :team_plannin
where(:text, :result) do
"#{image}{width=100cs}" | '<img src="example.jpg">'
"#{image}{width=auto height=200}" | '<img src="example.jpg" height="200">'
+ "#{image}{width=10000}" | '<img src="example.jpg">'
+ "#{image}{width=-200}" | '<img src="example.jpg">'
end
with_them do
diff --git a/spec/lib/banzai/filter/audio_link_filter_spec.rb b/spec/lib/banzai/filter/audio_link_filter_spec.rb
index 71e069eb29f..38d1ed40a84 100644
--- a/spec/lib/banzai/filter/audio_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/audio_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AudioLinkFilter do
+RSpec.describe Banzai::Filter::AudioLinkFilter, feature_category: :team_planning do
def filter(doc, contexts = {})
contexts.reverse_merge!({
project: project
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index 75108130602..2c75377ec42 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AutolinkFilter do
+RSpec.describe Banzai::Filter::AutolinkFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:link) { 'http://about.gitlab.com/' }
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
index 5712ed7da1f..575d4879f84 100644
--- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
+++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::BlockquoteFenceFilter do
+RSpec.describe Banzai::Filter::BlockquoteFenceFilter, feature_category: :team_planning do
include FilterSpecHelper
- it 'converts blockquote fences to blockquote lines' do
+ it 'converts blockquote fences to blockquote lines', :unlimited_max_formatted_output_length do
content = File.read(Rails.root.join('spec/fixtures/blockquote_fence_before.md'))
expected = File.read(Rails.root.join('spec/fixtures/blockquote_fence_after.md'))
diff --git a/spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb b/spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb
index c581750d2a9..fc88c5539ca 100644
--- a/spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb
+++ b/spec/lib/banzai/filter/broadcast_message_placeholders_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::BroadcastMessagePlaceholdersFilter do
+RSpec.describe Banzai::Filter::BroadcastMessagePlaceholdersFilter, feature_category: :team_planning do
include FilterSpecHelper
subject { filter(text, current_user: user, broadcast_message_placeholders: true).to_html }
diff --git a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb
index 67b480f8973..3b054862a26 100644
--- a/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/broadcast_message_sanitization_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter do
+RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter, feature_category: :team_planning do
include FilterSpecHelper
it_behaves_like 'default allowlist'
diff --git a/spec/lib/banzai/filter/color_filter_spec.rb b/spec/lib/banzai/filter/color_filter_spec.rb
index dced3671323..9a3f765b869 100644
--- a/spec/lib/banzai/filter/color_filter_spec.rb
+++ b/spec/lib/banzai/filter/color_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ColorFilter, lib: true do
+RSpec.describe Banzai::Filter::ColorFilter, feature_category: :team_planning, lib: true do
include FilterSpecHelper
let(:color) { '#F00' }
diff --git a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
index 6e29b910a6c..7fd25eac81b 100644
--- a/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/custom_emoji_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::CustomEmojiFilter do
+RSpec.describe Banzai::Filter::CustomEmojiFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:group) { create(:group) }
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index d621f63211b..1950b0f8bfe 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::EmojiFilter do
+RSpec.describe Banzai::Filter::EmojiFilter, feature_category: :team_planning do
include FilterSpecHelper
it_behaves_like 'emoji filter' do
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 036817834d5..3f72896939d 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.shared_examples 'an external link with rel attribute' do
+RSpec.shared_examples 'an external link with rel attribute', feature_category: :team_planning do
it 'adds rel="nofollow" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'nofollow'
diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb
index 26bca571fdc..4b765191449 100644
--- a/spec/lib/banzai/filter/footnote_filter_spec.rb
+++ b/spec/lib/banzai/filter/footnote_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::FootnoteFilter do
+RSpec.describe Banzai::Filter::FootnoteFilter, feature_category: :team_planning do
include FilterSpecHelper
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/banzai/filter/front_matter_filter_spec.rb b/spec/lib/banzai/filter/front_matter_filter_spec.rb
index f3543ab9582..b15f3ad2cd6 100644
--- a/spec/lib/banzai/filter/front_matter_filter_spec.rb
+++ b/spec/lib/banzai/filter/front_matter_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::FrontMatterFilter do
+RSpec.describe Banzai::Filter::FrontMatterFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'allows for `encoding:` before the front matter' do
@@ -114,7 +114,7 @@ RSpec.describe Banzai::Filter::FrontMatterFilter do
foo: :foo_symbol
- ---
+ ---
# Header
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 23626576c0c..f30262ef9df 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::GollumTagsFilter do
+RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
include FilterSpecHelper
let(:project) { create(:project) }
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
index d88fa21cde7..6de3fa50a1a 100644
--- a/spec/lib/banzai/filter/html_entity_filter_spec.rb
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::HtmlEntityFilter do
+RSpec.describe Banzai::Filter::HtmlEntityFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:unescaped) { 'foo <strike attr="foo">&&amp;&</strike>' }
diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
index 5b32be0ea62..c5bcfe1f384 100644
--- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ImageLazyLoadFilter do
+RSpec.describe Banzai::Filter::ImageLazyLoadFilter, feature_category: :team_planning do
include FilterSpecHelper
def image(path)
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index 78d68697ac7..2d496c447e1 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ImageLinkFilter do
+RSpec.describe Banzai::Filter::ImageLinkFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
index a11fe203541..1fdb29b688e 100644
--- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter do
+RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb b/spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb
index 287b5774048..5f971372dcc 100644
--- a/spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb
+++ b/spec/lib/banzai/filter/jira_import/adf_to_commonmark_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::JiraImport::AdfToCommonmarkFilter do
+RSpec.describe Banzai::Filter::JiraImport::AdfToCommonmarkFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb
index 3f4f3aafdd6..a528c5835b2 100644
--- a/spec/lib/banzai/filter/kroki_filter_spec.rb
+++ b/spec/lib/banzai/filter/kroki_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::KrokiFilter do
+RSpec.describe Banzai::Filter::KrokiFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'replaces nomnoml pre tag with img tag if kroki is enabled' do
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index e3c8d121587..c79cd58255d 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::MarkdownFilter do
+RSpec.describe Banzai::Filter::MarkdownFilter, feature_category: :team_planning do
include FilterSpecHelper
describe 'markdown engine from context' do
diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb
index c9bfcffe98f..de558a0774d 100644
--- a/spec/lib/banzai/filter/mermaid_filter_spec.rb
+++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::MermaidFilter do
+RSpec.describe Banzai::Filter::MermaidFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'adds `js-render-mermaid` class to the `code` tag' do
diff --git a/spec/lib/banzai/filter/normalize_source_filter_spec.rb b/spec/lib/banzai/filter/normalize_source_filter_spec.rb
index 8eaeec0e7b0..e267674e3b0 100644
--- a/spec/lib/banzai/filter/normalize_source_filter_spec.rb
+++ b/spec/lib/banzai/filter/normalize_source_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::NormalizeSourceFilter do
+RSpec.describe Banzai::Filter::NormalizeSourceFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'removes the UTF8 BOM from the beginning of the text' do
diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb
index 8186935f4b2..ec10306e8a8 100644
--- a/spec/lib/banzai/filter/output_safety_spec.rb
+++ b/spec/lib/banzai/filter/output_safety_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Banzai::Filter::OutputSafety do
+RSpec.describe Banzai::Filter::OutputSafety, feature_category: :team_planning do
subject do
Class.new do
include Banzai::Filter::OutputSafety
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index 4373af90cde..a1eabc23327 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::PlantumlFilter do
+RSpec.describe Banzai::Filter::PlantumlFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'replaces plantuml pre tag with img tag' do
diff --git a/spec/lib/banzai/filter/reference_redactor_filter_spec.rb b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb
index a2f34d42814..1d45b692374 100644
--- a/spec/lib/banzai/filter/reference_redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/reference_redactor_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ReferenceRedactorFilter do
+RSpec.describe Banzai::Filter::ReferenceRedactorFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'ignores non-GFM links' do
diff --git a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
index 3cb3ebc42a6..a8e08530fde 100644
--- a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
+RSpec.describe Banzai::Filter::References::AbstractReferenceFilter, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let(:doc) { Nokogiri::HTML.fragment('') }
diff --git a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
index c1fdee48f12..6ebf6c3cd1d 100644
--- a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::AlertReferenceFilter do
+RSpec.describe Banzai::Filter::References::AlertReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb
index b235de06b30..594a24fa279 100644
--- a/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter do
+RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter, feature_category: :source_code_management do
include FilterSpecHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
index c368a852ea9..73e3bf41ee9 100644
--- a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::CommitReferenceFilter do
+RSpec.describe Banzai::Filter::References::CommitReferenceFilter, feature_category: :source_code_management do
include FilterSpecHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/lib/banzai/filter/references/design_reference_filter_spec.rb b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb
index d616aabea45..08de9700cad 100644
--- a/spec/lib/banzai/filter/references/design_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
+RSpec.describe Banzai::Filter::References::DesignReferenceFilter, feature_category: :design_management do
include FilterSpecHelper
include DesignManagementTestHelpers
diff --git a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
index 2e811d35662..d40041d890e 100644
--- a/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do
+RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be_with_refind(:project) { create(:project) }
diff --git a/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb
index c64b66f746e..c2f4bf6caa5 100644
--- a/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter do
+RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter, feature_category: :feature_flags do
include FilterSpecHelper
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index 32538948b4b..d8a97c6c3dc 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
+RSpec.describe Banzai::Filter::References::IssueReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
include DesignManagementTestHelpers
@@ -47,57 +47,55 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
end
- context 'internal reference' do
- let(:reference) { "##{issue.iid}" }
-
+ shared_examples 'an internal reference' do
it_behaves_like 'a reference containing an element node'
it_behaves_like 'a reference with issue type information'
it 'links to a valid reference' do
- doc = reference_filter("Fixed #{reference}")
+ doc = reference_filter("Fixed #{written_reference}")
expect(doc.css('a').first.attr('href'))
.to eq issue_url
end
it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{written_reference}.)")
expect(doc.text).to eql("Fixed (#{reference}.)")
end
it 'ignores invalid issue IDs' do
- invalid = invalidate_reference(reference)
+ invalid = invalidate_reference(written_reference)
exp = act = "Fixed #{invalid}"
expect(reference_filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
- doc = reference_filter("Issue #{reference}")
+ doc = reference_filter("Issue #{written_reference}")
expect(doc.css('a').first.attr('title')).to eq issue.title
end
it 'escapes the title attribute' do
issue.update_attribute(:title, %{"></a>whatever<a title="})
- doc = reference_filter("Issue #{reference}")
+ doc = reference_filter("Issue #{written_reference}")
expect(doc.text).to eq "Issue #{reference}"
end
it 'renders non-HTML tooltips' do
- doc = reference_filter("Issue #{reference}")
+ doc = reference_filter("Issue #{written_reference}")
expect(doc.at_css('a')).not_to have_attribute('data-html')
end
it 'includes default classes' do
- doc = reference_filter("Issue #{reference}")
+ doc = reference_filter("Issue #{written_reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end
it 'includes a data-project attribute' do
- doc = reference_filter("Issue #{reference}")
+ doc = reference_filter("Issue #{written_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
@@ -105,7 +103,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'includes a data-issue attribute' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{written_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-issue')
@@ -113,7 +111,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'includes data attributes for issuable popover' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{written_reference}")
link = doc.css('a').first
expect(link.attr('data-project-path')).to eq project.full_path
@@ -121,21 +119,21 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'includes a data-original attribute' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{written_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-original')
- expect(link.attr('data-original')).to eq reference
+ expect(link.attr('data-original')).to eq written_reference
end
it 'does not escape the data-original attribute' do
inner_html = 'element <code>node</code> inside'
- doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>})
+ doc = reference_filter(%{<a href="#{written_reference}">#{inner_html}</a>})
expect(doc.children.first.attr('data-original')).to eq inner_html
end
it 'includes a data-reference-format attribute' do
- doc = reference_filter("Issue #{reference}+")
+ doc = reference_filter("Issue #{written_reference}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
@@ -153,7 +151,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'supports an :only_path context' do
- doc = reference_filter("Issue #{reference}", only_path: true)
+ doc = reference_filter("Issue #{written_reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
@@ -161,7 +159,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'does not process links containing issue numbers followed by text' do
- href = "#{reference}st"
+ href = "#{written_reference}st"
doc = reference_filter("<a href='#{href}'></a>")
link = doc.css('a').first.attr('href')
@@ -169,6 +167,20 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
end
+ context 'standard internal reference' do
+ let(:written_reference) { "##{issue.iid}" }
+ let(:reference) { "##{issue.iid}" }
+
+ it_behaves_like 'an internal reference'
+ end
+
+ context 'alternative internal_reference' do
+ let(:written_reference) { "GL-#{issue.iid}" }
+ let(:reference) { "##{issue.iid}" }
+
+ it_behaves_like 'an internal reference'
+ end
+
context 'cross-project / cross-namespace complete reference' do
let(:reference) { "#{project2.full_path}##{issue.iid}" }
let(:issue) { create(:issue, project: project2) }
diff --git a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
index d5b9c71b861..f8d223c6611 100644
--- a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'html/pipeline'
-RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
+RSpec.describe Banzai::Filter::References::LabelReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:project) { create(:project, :public, name: 'sample-project') }
diff --git a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
index 42e8cf1c857..9853d6f4093 100644
--- a/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
+RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter, feature_category: :code_review_workflow do
include FilterSpecHelper
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
index 98090af06b1..ecd5d1368c9 100644
--- a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
+RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:parent_group) { create(:group, :public) }
diff --git a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
index 0dd52b45f5d..49c71d08364 100644
--- a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
+RSpec.describe Banzai::Filter::References::ProjectReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
def invalidate_reference(reference)
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index dc43c33a08d..7307daca516 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::ReferenceCache do
+RSpec.describe Banzai::Filter::References::ReferenceCache, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:issue1) { create(:issue, project: project) }
diff --git a/spec/lib/banzai/filter/references/reference_filter_spec.rb b/spec/lib/banzai/filter/references/reference_filter_spec.rb
index 88404f2039d..b55b8fd41fa 100644
--- a/spec/lib/banzai/filter/references/reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::ReferenceFilter do
+RSpec.describe Banzai::Filter::References::ReferenceFilter, feature_category: :team_planning do
let(:project) { build_stubbed(:project) }
describe '#each_node' do
diff --git a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
index 2e324669870..32d1cb095d3 100644
--- a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do
+RSpec.describe Banzai::Filter::References::SnippetReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:project) { create(:project, :public) }
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 d61b71c711d..e248f2d9b1c 100644
--- a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::References::UserReferenceFilter do
+RSpec.describe Banzai::Filter::References::UserReferenceFilter, feature_category: :team_planning do
include FilterSpecHelper
def get_reference(user)
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 039ca36af6e..51832e60754 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::SanitizationFilter do
+RSpec.describe Banzai::Filter::SanitizationFilter, feature_category: :team_planning do
include FilterSpecHelper
it_behaves_like 'default allowlist'
diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
index 820ebeb6945..0d236cf2381 100644
--- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::SpacedLinkFilter do
+RSpec.describe Banzai::Filter::SpacedLinkFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:link) { '[example](page slug)' }
diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb
index d74bac4898e..e65a9214e76 100644
--- a/spec/lib/banzai/filter/suggestion_filter_spec.rb
+++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::SuggestionFilter do
+RSpec.describe Banzai::Filter::SuggestionFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion"><code>foo\n</code></pre>) }
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index b4be26ef8d2..0d7f322d08f 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
+RSpec.describe Banzai::Filter::SyntaxHighlightFilter, feature_category: :team_planning do
include FilterSpecHelper
shared_examples "XSS prevention" do |lang|
@@ -94,8 +94,9 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when #{lang} is specified" do
it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
+ copy_code_btn = '<copy-code></copy-code>' unless lang == 'suggestion'
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>#{copy_code_btn}</div>})
end
include_examples "XSS prevention", lang
@@ -107,8 +108,9 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "includes data-lang-params tag with extra information" do
result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
+ copy_code_btn = '<copy-code></copy-code>' unless lang == 'suggestion'
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>#{copy_code_btn}</div>})
end
include_examples "XSS prevention", lang
@@ -126,7 +128,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
let(:lang_params) { '-1+10' }
let(:expected_result) do
- %{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
+ %{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre></div>}
end
context 'when delimiter is space' do
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 91c644cb16a..26c949128da 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TableOfContentsFilter do
+RSpec.describe Banzai::Filter::TableOfContentsFilter, feature_category: :team_planning do
include FilterSpecHelper
def header(level, text)
diff --git a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
index 082e5c92e53..322225b38a9 100644
--- a/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_tag_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TableOfContentsTagFilter do
+RSpec.describe Banzai::Filter::TableOfContentsTagFilter, feature_category: :team_planning do
include FilterSpecHelper
context 'table of contents' do
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
index 920904b0f29..3eef6761153 100644
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TaskListFilter do
+RSpec.describe Banzai::Filter::TaskListFilter, feature_category: :team_planning do
include FilterSpecHelper
it 'adds `<task-button></task-button>` to every list item' do
diff --git a/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb
index 95d2e54459d..066f59758f0 100644
--- a/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb
+++ b/spec/lib/banzai/filter/timeout_html_pipeline_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TimeoutHtmlPipelineFilter do
+RSpec.describe Banzai::Filter::TimeoutHtmlPipelineFilter, feature_category: :team_planning do
include FilterSpecHelper
it_behaves_like 'filter timeout' do
diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
index 8970aa1d382..8ca6a1affdd 100644
--- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb
+++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TruncateSourceFilter do
+RSpec.describe Banzai::Filter::TruncateSourceFilter, feature_category: :team_planning do
include FilterSpecHelper
let(:short_text) { 'foo' * 10 }
diff --git a/spec/lib/banzai/filter/truncate_visible_filter_spec.rb b/spec/lib/banzai/filter/truncate_visible_filter_spec.rb
index 8daaed05264..404b23a886f 100644
--- a/spec/lib/banzai/filter/truncate_visible_filter_spec.rb
+++ b/spec/lib/banzai/filter/truncate_visible_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::TruncateVisibleFilter do
+RSpec.describe Banzai::Filter::TruncateVisibleFilter, feature_category: :team_planning do
include FilterSpecHelper
let_it_be(:project) { build(:project, :repository) }
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index eb45a8149c3..71656b6dc94 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::UploadLinkFilter do
+RSpec.describe Banzai::Filter::UploadLinkFilter, feature_category: :team_planning do
def filter(doc, contexts = {})
contexts.reverse_merge!(
project: project,
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index a0b0ba309f5..32d7f6cfefa 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::VideoLinkFilter do
+RSpec.describe Banzai::Filter::VideoLinkFilter, feature_category: :team_planning do
def filter(doc, contexts = {})
contexts.reverse_merge!({
project: project
@@ -11,16 +11,20 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
described_class.call(doc, contexts)
end
- def link_to_image(path)
+ def link_to_image(path, height = nil, width = nil)
return '<img/>' if path.nil?
- %(<img src="#{path}"/>)
+ attrs = %(src="#{path}")
+ attrs += %( width="#{width}") if width
+ attrs += %( height="#{height}") if height
+
+ %(<img #{attrs}/>)
end
let(:project) { create(:project, :repository) }
shared_examples 'a video element' do
- let(:image) { link_to_image(src) }
+ let(:image) { link_to_image(src, height, width) }
it 'replaces the image tag with a video tag' do
container = filter(image).children.first
@@ -32,7 +36,9 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
expect(video.name).to eq 'video'
expect(video['src']).to eq src
- expect(video['width']).to eq "400"
+ expect(video['height']).to eq height if height
+ expect(video['width']).to eq width if width
+ expect(video['width']).to eq '400' unless width || height
expect(video['preload']).to eq 'metadata'
expect(link.name).to eq 'a'
@@ -51,6 +57,9 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
end
context 'when the element src has a video extension' do
+ let(:height) { nil }
+ let(:width) { nil }
+
Gitlab::FileTypeDetection::SAFE_VIDEO_EXT.each do |ext|
it_behaves_like 'a video element' do
let(:src) { "/path/video.#{ext}" }
@@ -62,6 +71,25 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
end
end
+ context 'when the element has height or width specified' do
+ let(:src) { '/path/video.mp4' }
+
+ it_behaves_like 'a video element' do
+ let(:height) { '100%' }
+ let(:width) { '50px' }
+ end
+
+ it_behaves_like 'a video element' do
+ let(:height) { nil }
+ let(:width) { '50px' }
+ end
+
+ it_behaves_like 'a video element' do
+ let(:height) { '50px' }
+ let(:width) { nil }
+ end
+ end
+
context 'when the element has no src attribute' do
let(:src) { nil }
@@ -85,6 +113,8 @@ RSpec.describe Banzai::Filter::VideoLinkFilter do
context 'and src is a video' do
let(:src) { '/path/video.mp4' }
+ let(:height) { nil }
+ let(:width) { nil }
it_behaves_like 'a video element'
end
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index 9807e385a5a..ace3aea5346 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::WikiLinkFilter do
+RSpec.describe Banzai::Filter::WikiLinkFilter, feature_category: :wiki do
include FilterSpecHelper
let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
diff --git a/spec/lib/banzai/filter_array_spec.rb b/spec/lib/banzai/filter_array_spec.rb
index f341d5d51a0..bb457568bee 100644
--- a/spec/lib/banzai/filter_array_spec.rb
+++ b/spec/lib/banzai/filter_array_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Banzai::FilterArray do
+RSpec.describe Banzai::FilterArray, feature_category: :team_planning do
describe '#insert_after' do
it 'inserts an element after a provided element' do
filters = described_class.new(%w(a b c))
diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb
index 8fec9691d7f..b2c869bd066 100644
--- a/spec/lib/banzai/issuable_extractor_spec.rb
+++ b/spec/lib/banzai/issuable_extractor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::IssuableExtractor do
+RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:extractor) { described_class.new(Banzai::RenderContext.new(project, user)) }
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 8f69480c65f..b2f7db24e38 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ObjectRenderer do
+RSpec.describe Banzai::ObjectRenderer, feature_category: :team_planning do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:renderer) do
diff --git a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
index ad4256c2045..9f1af821d11 100644
--- a/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/broadcast_message_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline do
+RSpec.describe Banzai::Pipeline::BroadcastMessagePipeline, feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
index be553433e9e..fa25612a06e 100644
--- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::DescriptionPipeline do
+RSpec.describe Banzai::Pipeline::DescriptionPipeline, feature_category: :team_planning do
let_it_be(:project) { create(:project) }
def parse(html)
diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
index c7a0b9fefa1..4d2dca84b1b 100644
--- a/spec/lib/banzai/pipeline/email_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::EmailPipeline do
+RSpec.describe Banzai::Pipeline::EmailPipeline, feature_category: :team_planning do
describe '.filters' do
it 'returns the expected type' do
expect(described_class.filters).to be_kind_of(Banzai::FilterArray)
diff --git a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
index 8103846d4f7..6ecd7f56dec 100644
--- a/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/emoji_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::EmojiPipeline do
+RSpec.describe Banzai::Pipeline::EmojiPipeline, feature_category: :team_planning do
let(:emoji) { TanukiEmoji.find_by_alpha_code('100') }
def parse(text)
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
index c1d5f16b562..ca05a353d47 100644
--- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning do
+ using RSpec::Parameterized::TableSyntax
+
describe 'References' do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
@@ -157,14 +159,44 @@ RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :team_planning
markdown = "\\#{issue.to_reference}"
output = described_class.to_html(markdown, project: project)
- expect(output).to include("<span>#</span>#{issue.iid}")
+ expect(output).to include("<span data-escaped-char>#</span>#{issue.iid}")
end
it 'converts user reference with escaped underscore because of italics' do
markdown = '_@test\__'
output = described_class.to_html(markdown, project: project)
- expect(output).to include('<em>@test<span>_</span></em>')
+ expect(output).to include('<em>@test_</em>')
+ end
+
+ context 'when a reference (such as a label name) is autocompleted with characters that require escaping' do
+ # Labels are fairly representative of the type of characters that can be in a reference
+ # and aligns with the testing in spec/frontend/gfm_auto_complete_spec.js
+ where(:valid, :label_name, :markdown) do
+ # These are currently not supported
+ # true | 'a~bug' | '~"a\~bug"'
+ # true | 'b~~bug~~' | '~"b\~\~bug\~\~"'
+
+ true | 'c_bug_' | '~c_bug\_'
+ true | 'c_bug_' | 'Label ~c_bug\_ and _more_ text'
+ true | 'd _bug_' | '~"d \_bug\_"'
+ true | 'e*bug*' | '~"e\*bug\*"'
+ true | 'f *bug*' | '~"f \*bug\*"'
+ true | 'f *bug*' | 'Label ~"f \*bug\*" **with** more text'
+ true | 'g`bug`' | '~"g\`bug\`" '
+ true | 'h `bug`' | '~"h \`bug\`"'
+ end
+
+ with_them do
+ it 'detects valid escaped reference' do
+ create(:label, name: label_name, project: project)
+
+ result = Banzai::Pipeline::FullPipeline.call(markdown, project: project)
+
+ expect(result[:output].css('a').first.attr('class')).to eq 'gfm gfm-label has-tooltip gl-link gl-label-link'
+ expect(result[:output].css('a').first.content).to eq label_name
+ end
+ end
end
end
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
index f67f13b3862..d0b85a1d043 100644
--- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::GfmPipeline do
+RSpec.describe Banzai::Pipeline::GfmPipeline, feature_category: :team_planning do
describe 'integration between parsing regular and external issue references' do
let(:project) { create(:project, :with_redmine_integration, :public) }
diff --git a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
index 74005adf673..f579b9e1883 100644
--- a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline do
+RSpec.describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline, feature_category: :team_planning do
let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' }
it 'converts text in Atlassian Document Format' do
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index 0e4a4e4492e..e7c15ed9cf6 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -15,10 +15,15 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline, feature_category: :team_
result = described_class.call(markdown, project: project)
output = result[:output].to_html
- Banzai::Filter::MarkdownPreEscapeFilter::ESCAPABLE_CHARS.pluck(:char).each do |char|
- char = '&amp;' if char == '&'
-
- expect(output).to include("<span>#{char}</span>")
+ Banzai::Filter::MarkdownPreEscapeFilter::ESCAPABLE_CHARS.each do |item|
+ char = item[:char] == '&' ? '&amp;' : item[:char]
+
+ if item[:reference]
+ expect(output).to include("<span data-escaped-char>#{char}</span>")
+ else
+ expect(output).not_to include("<span data-escaped-char>#{char}</span>")
+ expect(output).to include(char)
+ end
end
expect(result[:escaped_literals]).to be_truthy
diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
index e8df395564a..072d77f4112 100644
--- a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::PostProcessPipeline do
+RSpec.describe Banzai::Pipeline::PostProcessPipeline, feature_category: :team_planning do
subject { described_class.call(doc, context) }
let_it_be(:project) { create(:project, :public, :repository) }
diff --git a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
index 303d0fcb6c2..55575d4cf84 100644
--- a/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/pre_process_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::PreProcessPipeline do
+RSpec.describe Banzai::Pipeline::PreProcessPipeline, feature_category: :team_planning do
it 'pre-processes the source text' do
markdown = <<~MD
\xEF\xBB\xBF---
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 59f5e4a6900..837ea2d7bc0 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Pipeline::WikiPipeline do
+RSpec.describe Banzai::Pipeline::WikiPipeline, feature_category: :wiki do
let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let_it_be(:wiki) { ProjectWiki.new(project, nil) }
diff --git a/spec/lib/banzai/pipeline_spec.rb b/spec/lib/banzai/pipeline_spec.rb
index b2c970e4394..c1199654cd6 100644
--- a/spec/lib/banzai/pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Banzai::Pipeline do
+RSpec.describe Banzai::Pipeline, feature_category: :team_planning do
describe '.[]' do
subject { described_class[name] }
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
index fc7aaa94954..8f95816b073 100644
--- a/spec/lib/banzai/querying_spec.rb
+++ b/spec/lib/banzai/querying_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Banzai::Querying do
+RSpec.describe Banzai::Querying, feature_category: :team_planning do
describe '.css' do
it 'optimizes queries for elements with classes' do
document = double(:document)
diff --git a/spec/lib/banzai/reference_parser/alert_parser_spec.rb b/spec/lib/banzai/reference_parser/alert_parser_spec.rb
index 0a9499fe6e4..33d7a7130dd 100644
--- a/spec/lib/banzai/reference_parser/alert_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/alert_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::AlertParser do
+RSpec.describe Banzai::ReferenceParser::AlertParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index 61751b69842..bc7a93a7cde 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::BaseParser do
+RSpec.describe Banzai::ReferenceParser::BaseParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:user) { create(:user) }
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 3569a1019f0..081bfa26fb2 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::CommitParser do
+RSpec.describe Banzai::ReferenceParser::CommitParser, feature_category: :source_code_management do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index 172347fc421..e058793c659 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::CommitRangeParser do
+RSpec.describe Banzai::ReferenceParser::CommitRangeParser, feature_category: :source_code_management do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/design_parser_spec.rb b/spec/lib/banzai/reference_parser/design_parser_spec.rb
index a9cb2952c26..c490cf5b36d 100644
--- a/spec/lib/banzai/reference_parser/design_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/design_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::DesignParser do
+RSpec.describe Banzai::ReferenceParser::DesignParser, feature_category: :design_management do
include ReferenceParserHelpers
include DesignManagementTestHelpers
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index 0c1b98e5ec3..1b2c9792cf7 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::ExternalIssueParser do
+RSpec.describe Banzai::ReferenceParser::ExternalIssueParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
index 288eb9ae360..ba71949ee44 100644
--- a/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/feature_flag_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::FeatureFlagParser do
+RSpec.describe Banzai::ReferenceParser::FeatureFlagParser, feature_category: :feature_flags do
include ReferenceParserHelpers
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index c180a42c91e..2efdb928b6f 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::IssueParser do
+RSpec.describe Banzai::ReferenceParser::IssueParser, feature_category: :team_planning do
include ReferenceParserHelpers
let_it_be(:group) { create(:group, :public) }
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index 8f287e15b37..e27af57f15d 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::LabelParser do
+RSpec.describe Banzai::ReferenceParser::LabelParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb
index 576e629d271..c5302d52270 100644
--- a/spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/mentioned_group_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::MentionedGroupParser do
+RSpec.describe Banzai::ReferenceParser::MentionedGroupParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:group) { create(:group, :private) }
diff --git a/spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb
index 983407addce..242a1720f39 100644
--- a/spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/mentioned_project_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::MentionedProjectParser do
+RSpec.describe Banzai::ReferenceParser::MentionedProjectParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:group) { create(:group, :private) }
diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
index f117d796dad..14a5bf04752 100644
--- a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::MentionedUserParser do
+RSpec.describe Banzai::ReferenceParser::MentionedUserParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:group) { create(:group, :private) }
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index 3fbda7f3239..eead5019217 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
+RSpec.describe Banzai::ReferenceParser::MergeRequestParser, feature_category: :code_review_workflow do
include ReferenceParserHelpers
let(:group) { create(:group, :public) }
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 95f71154e38..1e7a492cb35 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::MilestoneParser do
+RSpec.describe Banzai::ReferenceParser::MilestoneParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb
index 2c0b6c417b0..90a3660a0e3 100644
--- a/spec/lib/banzai/reference_parser/project_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::ProjectParser do
+RSpec.describe Banzai::ReferenceParser::ProjectParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index e8ef4e7f6e3..8f4148be2dc 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::SnippetParser do
+RSpec.describe Banzai::ReferenceParser::SnippetParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index d4f4339cf17..179e6e73fa3 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceParser::UserParser do
+RSpec.describe Banzai::ReferenceParser::UserParser, feature_category: :team_planning do
include ReferenceParserHelpers
let(:group) { create(:group) }
diff --git a/spec/lib/banzai/reference_redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb
index 344b8988296..8a8f3ce586a 100644
--- a/spec/lib/banzai/reference_redactor_spec.rb
+++ b/spec/lib/banzai/reference_redactor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::ReferenceRedactor do
+RSpec.describe Banzai::ReferenceRedactor, feature_category: :team_planning do
let(:user) { create(:user) }
let(:project) { build(:project) }
let(:redactor) { described_class.new(Banzai::RenderContext.new(project, user)) }
diff --git a/spec/lib/banzai/render_context_spec.rb b/spec/lib/banzai/render_context_spec.rb
index 4b5c2c5a7df..76423b5805c 100644
--- a/spec/lib/banzai/render_context_spec.rb
+++ b/spec/lib/banzai/render_context_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::RenderContext do
+RSpec.describe Banzai::RenderContext, feature_category: :team_planning do
let(:document) { Nokogiri::HTML.fragment('<p>hello</p>') }
describe '#project_for_node' do
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 705f44baf16..8c9d8d51d5f 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Renderer do
+RSpec.describe Banzai::Renderer, feature_category: :team_planning do
let(:renderer) { described_class }
def fake_object(fresh:)
diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb
index a5b5e96e594..58e6992698c 100644
--- a/spec/lib/bulk_imports/clients/graphql_spec.rb
+++ b/spec/lib/bulk_imports/clients/graphql_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Clients::Graphql do
+RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
let_it_be(:config) { create(:bulk_import_configuration) }
subject { described_class.new(url: config.url, token: config.access_token) }
@@ -11,30 +11,60 @@ RSpec.describe BulkImports::Clients::Graphql do
let(:query) { '{ metadata { version } }' }
let(:graphql_client_double) { double }
let(:response_double) { double }
+ let(:version) { '14.0.0' }
before do
stub_const('BulkImports::MINIMUM_COMPATIBLE_MAJOR_VERSION', version)
- allow(graphql_client_double).to receive(:execute)
- allow(subject).to receive(:client).and_return(graphql_client_double)
- allow(graphql_client_double).to receive(:execute).with(query).and_return(response_double)
- allow(response_double).to receive_message_chain(:data, :metadata, :version).and_return(version)
end
- context 'when source instance is compatible' do
- let(:version) { '14.0.0' }
+ describe 'source instance validation' do
+ before do
+ allow(graphql_client_double).to receive(:execute)
+ allow(subject).to receive(:client).and_return(graphql_client_double)
+ allow(graphql_client_double).to receive(:execute).with(query).and_return(response_double)
+ allow(response_double).to receive_message_chain(:data, :metadata, :version).and_return(version)
+ end
- it 'marks source instance as compatible' do
- subject.execute('test')
+ context 'when source instance is compatible' do
+ it 'marks source instance as compatible' do
+ subject.execute('test')
- expect(subject.instance_variable_get(:@compatible_instance_version)).to eq(true)
+ expect(subject.instance_variable_get(:@compatible_instance_version)).to eq(true)
+ end
+ end
+
+ context 'when source instance is incompatible' do
+ let(:version) { '13.0.0' }
+
+ it 'raises an error' do
+ expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} or later.")
+ end
end
end
- context 'when source instance is incompatible' do
- let(:version) { '13.0.0' }
+ describe 'network errors' do
+ before do
+ allow(Gitlab::HTTP)
+ .to receive(:post)
+ .and_return(response_double)
+ end
+
+ context 'when response cannot be parsed' do
+ let(:response_double) { instance_double(HTTParty::Response, body: 'invalid', success?: true) }
+
+ it 'raises network error' do
+ expect { subject.execute('test') }.to raise_error(BulkImports::NetworkError, /unexpected character/)
+ end
+ end
+
+ context 'when response is unsuccessful' do
+ let(:response_double) { instance_double(HTTParty::Response, success?: false, code: 503) }
+
+ it 'raises network error' do
+ allow(response_double).to receive_message_chain(:request, :path, :path).and_return('foo/bar')
- it 'raises an error' do
- expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab Version. Minimum Supported Gitlab Version #{BulkImport::MIN_MAJOR_VERSION}.")
+ expect { subject.execute('test') }.to raise_error(BulkImports::NetworkError, 'Unsuccessful response 503 from foo/bar')
+ end
end
end
end
diff --git a/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb
index 241bd694a2c..badc4a45c86 100644
--- a/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Common::Pipelines::BoardsPipeline do
+RSpec.describe BulkImports::Common::Pipelines::BoardsPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
@@ -53,7 +53,7 @@ RSpec.describe BulkImports::Common::Pipelines::BoardsPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
+ destination_slug: 'My-Destination-Group',
destination_namespace: group.full_path
)
end
@@ -78,7 +78,7 @@ RSpec.describe BulkImports::Common::Pipelines::BoardsPipeline do
group: group,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
+ destination_slug: 'My-Destination-Group',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb
index ac516418ce8..23368f38889 100644
--- a/spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/labels_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline do
+RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -13,7 +13,7 @@ RSpec.describe BulkImports::Common::Pipelines::LabelsPipeline do
group: group,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
+ destination_slug: 'My-Destination-Group',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb
index 902b29bc365..8a61e927712 100644
--- a/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/milestones_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline do
+RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline, feature_category: :importers do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import, user: user) }
@@ -97,7 +97,7 @@ RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline do
group: group,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
+ destination_slug: 'My-Destination-Group',
destination_namespace: group.full_path
)
end
@@ -119,7 +119,7 @@ RSpec.describe BulkImports::Common::Pipelines::MilestonesPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/uploads_pipeline_spec.rb
index 35ca67c8a4c..d6622785e65 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, feature_category: :import do
+RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
diff --git a/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb b/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
index 0eefb7390dc..30eef0b9f9e 100644
--- a/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Common::Pipelines::WikiPipeline do
+RSpec.describe BulkImports::Common::Pipelines::WikiPipeline, feature_category: :importers do
describe '#run' do
let_it_be(:user) { create(:user) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Common::Pipelines::WikiPipeline do
:project_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Wiki',
+ destination_slug: 'My-Destination-Wiki',
destination_namespace: parent.full_path,
project: parent
)
diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
index 69363bf0866..7d1f9ae5da0 100644
--- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
+++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
+RSpec.describe BulkImports::Groups::Loaders::GroupLoader, feature_category: :importers do
describe '#load' do
let_it_be(:user) { create(:user) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -29,19 +29,9 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
end
end
- context 'when group exists' do
- it 'raises an error' do
- group1 = create(:group)
- group2 = create(:group, parent: group1)
- entity.update!(destination_namespace: group1.full_path)
- data = { 'path' => group2.path }
-
- expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Group exists')
- end
- end
-
context 'when there are other group errors' do
it 'raises an error with those errors' do
+ entity.update!(destination_namespace: '')
group = ::Group.new
group.validate
expected_errors = group.errors.full_messages.to_sentence
diff --git a/spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb
index c07d27e973f..395f3568913 100644
--- a/spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/project_entities_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Groups::Pipelines::ProjectEntitiesPipeline do
+RSpec.describe BulkImports::Groups::Pipelines::ProjectEntitiesPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:destination_group) { create(:group) }
@@ -20,8 +20,9 @@ RSpec.describe BulkImports::Groups::Pipelines::ProjectEntitiesPipeline do
let(:extracted_data) do
BulkImports::Pipeline::ExtractedData.new(data: {
'id' => 'gid://gitlab/Project/1234567',
- 'name' => 'project',
- 'full_path' => 'group/project'
+ 'name' => 'My Project',
+ 'path' => 'my-project',
+ 'full_path' => 'group/my-project'
})
end
@@ -42,8 +43,9 @@ RSpec.describe BulkImports::Groups::Pipelines::ProjectEntitiesPipeline do
project_entity = BulkImports::Entity.last
expect(project_entity.source_type).to eq('project_entity')
- expect(project_entity.source_full_path).to eq('group/project')
- expect(project_entity.destination_name).to eq('project')
+ expect(project_entity.source_full_path).to eq('group/my-project')
+ expect(project_entity.destination_slug).to eq('my-project')
+ expect(project_entity.destination_name).to eq('my-project')
expect(project_entity.destination_namespace).to eq(destination_group.full_path)
expect(project_entity.source_xid).to eq(1234567)
end
diff --git a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
index 32d8dc8e207..138a92a7e6b 100644
--- a/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/groups/transformers/group_attributes_transformer_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
+RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer, feature_category: :importers do
describe '#transform' do
- let_it_be(:parent) { create(:group) }
-
let(:bulk_import) { build_stubbed(:bulk_import) }
+ let(:destination_group) { create(:group) }
+ let(:destination_namespace) { destination_group.full_path }
let(:entity) do
build_stubbed(
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_slug: 'destination-slug-path',
- destination_namespace: parent.full_path
+ destination_namespace: destination_namespace
)
end
@@ -40,15 +40,13 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
}
end
- subject { described_class.new }
+ subject(:transformed_data) { described_class.new.transform(context, data) }
it 'returns original data with some keys transformed' do
- transformed_data = subject.transform(context, data)
-
expect(transformed_data).to eq({
'name' => 'Source Group Name',
'description' => 'Source Group Description',
- 'parent_id' => parent.id,
+ 'parent_id' => destination_group.id,
'path' => entity.destination_slug,
'visibility_level' => Gitlab::VisibilityLevel.string_options[data['visibility']],
'project_creation_level' => Gitlab::Access.project_creation_string_options[data['project_creation_level']],
@@ -64,21 +62,21 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
end
context 'when some fields are not present' do
- it 'does not include those fields' do
- data = {
+ let(:data) do
+ {
'name' => 'Source Group Name',
'description' => 'Source Group Description',
'path' => 'source-group-path',
'full_path' => 'source/full/path'
}
+ end
- transformed_data = subject.transform(context, data)
-
+ it 'does not include those fields' do
expect(transformed_data).to eq({
'name' => 'Source Group Name',
'path' => 'destination-slug-path',
'description' => 'Source Group Description',
- 'parent_id' => parent.id,
+ 'parent_id' => destination_group.id,
'share_with_group_lock' => nil,
'emails_disabled' => nil,
'lfs_enabled' => nil,
@@ -89,9 +87,7 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
describe 'parent group transformation' do
it 'sets parent id' do
- transformed_data = subject.transform(context, data)
-
- expect(transformed_data['parent_id']).to eq(parent.id)
+ expect(transformed_data['parent_id']).to eq(destination_group.id)
end
context 'when destination namespace is empty' do
@@ -100,8 +96,6 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
end
it 'does not set parent id' do
- transformed_data = subject.transform(context, data)
-
expect(transformed_data).not_to have_key('parent_id')
end
end
@@ -114,8 +108,6 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
end
it 'does not transform name' do
- transformed_data = subject.transform(context, data)
-
expect(transformed_data['name']).to eq('Source Group Name')
end
end
@@ -123,35 +115,39 @@ RSpec.describe BulkImports::Groups::Transformers::GroupAttributesTransformer do
context 'when destination namespace is present' do
context 'when destination namespace does not have a group with same name' do
it 'does not transform name' do
- transformed_data = subject.transform(context, data)
-
expect(transformed_data['name']).to eq('Source Group Name')
end
end
context 'when destination namespace already have a group with the same name' do
before do
- create(:group, parent: parent, name: 'Source Group Name', path: 'group_1')
- create(:group, parent: parent, name: 'Source Group Name(1)', path: 'group_2')
- create(:group, parent: parent, name: 'Source Group Name(2)', path: 'group_3')
- create(:group, parent: parent, name: 'Source Group Name(1)(1)', path: 'group_4')
+ create(:group, parent: destination_group, name: 'Source Group Name', path: 'group_1')
+ create(:group, parent: destination_group, name: 'Source Group Name(1)', path: 'group_2')
+ create(:group, parent: destination_group, name: 'Source Group Name(2)', path: 'group_3')
+ create(:group, parent: destination_group, name: 'Source Group Name(1)(1)', path: 'group_4')
end
it 'makes the name unique by appeding a counter', :aggregate_failures do
- transformed_data = subject.transform(context, data.merge('name' => 'Source Group Name'))
+ transformed_data = described_class.new.transform(context, data.merge('name' => 'Source Group Name'))
expect(transformed_data['name']).to eq('Source Group Name(3)')
- transformed_data = subject.transform(context, data.merge('name' => 'Source Group Name(2)'))
+ transformed_data = described_class.new.transform(context, data.merge('name' => 'Source Group Name(2)'))
expect(transformed_data['name']).to eq('Source Group Name(2)(1)')
- transformed_data = subject.transform(context, data.merge('name' => 'Source Group Name(1)'))
+ transformed_data = described_class.new.transform(context, data.merge('name' => 'Source Group Name(1)'))
expect(transformed_data['name']).to eq('Source Group Name(1)(2)')
- transformed_data = subject.transform(context, data.merge('name' => 'Source Group Name(1)(1)'))
+ transformed_data = described_class.new.transform(context, data.merge('name' => 'Source Group Name(1)(1)'))
expect(transformed_data['name']).to eq('Source Group Name(1)(1)(1)')
end
end
end
end
+
+ describe 'visibility level' do
+ subject(:transformed_data) { described_class.new.transform(context, data) }
+
+ include_examples 'visibility level settings'
+ end
end
end
diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb
index 83d6f494d53..0f1d00172cd 100644
--- a/spec/lib/bulk_imports/pipeline/context_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/context_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Pipeline::Context do
create(
:bulk_import_entity,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Group',
+ destination_slug: 'My-Destination-Group',
destination_namespace: group.full_path,
group: group,
bulk_import: bulk_import
diff --git a/spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb
index e2744a6a457..b35ba612197 100644
--- a/spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/auto_devops_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::AutoDevopsPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
index 98a2e8b6a57..a78f524b227 100644
--- a/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/ci_pipelines_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::CiPipelinesPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
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 19e3a1fecc3..a0789522ea8 100644
--- a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
index e780cde4ae2..5b85b3eee79 100644
--- a/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/merge_requests_pipeline_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::Projects::Pipelines::MergeRequestsPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb
index 12713f008bb..0bfd9410808 100644
--- a/spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/pipeline_schedules_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::PipelineSchedulesPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb
index 567a0a4fcc3..09385a261b6 100644
--- a/spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/project_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ProjectPipeline do
source_type: :project_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb
index 3c3d0a6d1c4..895d37ea385 100644
--- a/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -19,11 +19,44 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
-
let(:issue) { create(:issue, project: project, description: 'https://my.gitlab.com/source/full/path/-/issues/1') }
- let(:mr) { create(:merge_request, source_project: project, description: 'https://my.gitlab.com/source/full/path/-/merge_requests/1') }
- let(:issue_note) { create(:note, project: project, noteable: issue, note: 'https://my.gitlab.com/source/full/path/-/issues/1') }
- let(:mr_note) { create(:note, project: project, noteable: mr, note: 'https://my.gitlab.com/source/full/path/-/merge_requests/1') }
+ let(:mr) do
+ create(
+ :merge_request,
+ source_project: project,
+ description: 'https://my.gitlab.com/source/full/path/-/merge_requests/1'
+ )
+ end
+
+ let(:issue_note) do
+ create(
+ :note,
+ project: project,
+ noteable: issue,
+ note: 'https://my.gitlab.com/source/full/path/-/issues/1'
+ )
+ end
+
+ let(:mr_note) do
+ create(
+ :note,
+ project: project,
+ noteable: mr,
+ note: 'https://my.gitlab.com/source/full/path/-/merge_requests/1'
+ )
+ end
+
+ let(:old_note_html) { 'old note_html' }
+ let(:system_note) do
+ create(
+ :note,
+ project: project,
+ system: true,
+ noteable: issue,
+ note: "mentioned in merge request !#{mr.iid}",
+ note_html: old_note_html
+ )
+ end
subject(:pipeline) { described_class.new(context) }
@@ -32,7 +65,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
end
def create_project_data
- [issue, mr, issue_note, mr_note]
+ [issue, mr, issue_note, mr_note, system_note]
end
describe '#extract' do
@@ -43,6 +76,10 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
expect(extracted_data.data).to contain_exactly(issue_note, mr, issue, mr_note)
+ expect(system_note.note_html).not_to eq(old_note_html)
+ expect(system_note.note_html)
+ .to include("class=\"gfm gfm-merge_request\">!#{mr.iid}</a></p>")
+ .and include(project.full_path.to_s)
end
end
@@ -122,9 +159,11 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
it 'does not save the object' do
expect(mr).not_to receive(:save!)
expect(mr_note).not_to receive(:save!)
+ expect(system_note).not_to receive(:save!)
subject.load(context, mr)
subject.load(context, mr_note)
+ subject.load(context, system_note)
end
end
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
index a376cdd712c..339ca727b57 100644
--- a/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/releases_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ReleasesPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
index a968104fc91..0cc8da80a61 100644
--- a/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/repository_pipeline_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Pipelines::RepositoryPipeline do
+RSpec.describe BulkImports::Projects::Pipelines::RepositoryPipeline, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:parent) { create(:project) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::RepositoryPipeline do
:project_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Repository',
+ destination_slug: 'My-Destination-Repository',
destination_namespace: parent.full_path,
project: parent
)
diff --git a/spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb
index dae879de998..41b3ea37804 100644
--- a/spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsPipeline do
project: project,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: group.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb
index dfd01cdf4bb..56c0f8c8807 100644
--- a/spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline do
project: project,
bulk_import: bulk_import_configuration.bulk_import,
source_full_path: 'source/full/path',
- destination_name: 'My Destination Project',
+ destination_slug: 'My-Destination-Project',
destination_namespace: project.full_path
)
end
diff --git a/spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb b/spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb
index c1c4d0bf0db..36dc63a9331 100644
--- a/spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/projects/transformers/project_attributes_transformer_spec.rb
@@ -2,27 +2,27 @@
require 'spec_helper'
-RSpec.describe BulkImports::Projects::Transformers::ProjectAttributesTransformer do
+RSpec.describe BulkImports::Projects::Transformers::ProjectAttributesTransformer, feature_category: :importers do
describe '#transform' do
let_it_be(:user) { create(:user) }
- let_it_be(:destination_group) { create(:group) }
let_it_be(:project) { create(:project, name: 'My Source Project') }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
- let_it_be(:entity) do
+ let(:entity) do
create(
:bulk_import_entity,
source_type: :project_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_slug: 'Destination Project Name',
- destination_namespace: destination_group.full_path
+ destination_slug: 'Destination-Project-Name',
+ destination_namespace: destination_namespace
)
end
- let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
- let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
-
+ let(:destination_group) { create(:group) }
+ let(:destination_namespace) { destination_group.full_path }
+ let(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:data) do
{
'visibility' => 'private',
@@ -40,13 +40,6 @@ RSpec.describe BulkImports::Projects::Transformers::ProjectAttributesTransformer
expect(transformed_data[:path]).to eq(entity.destination_slug.parameterize)
end
- it 'transforms visibility level' do
- visibility = data['visibility']
-
- expect(transformed_data).not_to have_key(:visibility)
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel.string_options[visibility])
- end
-
it 'adds import type' do
expect(transformed_data[:import_type]).to eq(described_class::PROJECT_IMPORT_TYPE)
end
@@ -65,7 +58,7 @@ RSpec.describe BulkImports::Projects::Transformers::ProjectAttributesTransformer
source_type: :project_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
- destination_slug: 'Destination Project Name',
+ destination_slug: 'Destination-Project-Name',
destination_namespace: ''
)
@@ -89,8 +82,12 @@ RSpec.describe BulkImports::Projects::Transformers::ProjectAttributesTransformer
transformed_data = described_class.new.transform(context, data)
expect(transformed_data.keys)
- .to contain_exactly(:created_at, :import_type, :name, :namespace_id, :path, :visibility_level)
+ .to contain_exactly('created_at', 'import_type', 'name', 'namespace_id', 'path', 'visibility_level')
end
end
+
+ describe 'visibility level' do
+ include_examples 'visibility level settings'
+ end
end
end
diff --git a/spec/lib/extracts_ref_spec.rb b/spec/lib/extracts_ref_spec.rb
index ca8af9413f3..93a09bf5a0a 100644
--- a/spec/lib/extracts_ref_spec.rb
+++ b/spec/lib/extracts_ref_spec.rb
@@ -48,13 +48,11 @@ RSpec.describe ExtractsRef do
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
+ 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
diff --git a/spec/lib/feature_groups/gitlab_team_members_spec.rb b/spec/lib/feature_groups/gitlab_team_members_spec.rb
new file mode 100644
index 00000000000..f4db02e6c58
--- /dev/null
+++ b/spec/lib/feature_groups/gitlab_team_members_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FeatureGroups::GitlabTeamMembers, feature_category: :shared do
+ let_it_be(:gitlab_com) { create(:group) }
+ let_it_be_with_reload(:member) { create(:user).tap { |user| gitlab_com.add_developer(user) } }
+ let_it_be_with_reload(:non_member) { create(:user) }
+
+ before do
+ stub_const("#{described_class.name}::GITLAB_COM_GROUP_ID", gitlab_com.id)
+ end
+
+ describe '#enabled?' do
+ context 'when not on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(described_class.enabled?(member)).to eq(false)
+ end
+ end
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'returns true for gitlab-com group members' do
+ expect(described_class.enabled?(member)).to eq(true)
+ end
+
+ it 'returns false for users not in gitlab-com' do
+ expect(described_class.enabled?(non_member)).to eq(false)
+ end
+
+ it 'returns false when actor is not a user' do
+ expect(described_class.enabled?(gitlab_com)).to eq(false)
+ end
+
+ it 'reloads members after 1 hour' do
+ expect(described_class.enabled?(non_member)).to eq(false)
+
+ gitlab_com.add_developer(non_member)
+
+ travel_to(2.hours.from_now) do
+ expect(described_class.enabled?(non_member)).to eq(true)
+ end
+ end
+
+ it 'does not make queries on subsequent calls', :use_clean_rails_memory_store_caching do
+ described_class.enabled?(member)
+ non_member
+
+ queries = ActiveRecord::QueryRecorder.new do
+ described_class.enabled?(member)
+ described_class.enabled?(non_member)
+ end
+
+ expect(queries.count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index c087931d36a..c86bc36057a 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Feature, stub_feature_flags: false do
+RSpec.describe Feature, stub_feature_flags: false, feature_category: :shared do
include StubVersion
before do
@@ -154,6 +154,17 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
+ describe '.register_feature_groups' do
+ before do
+ Flipper.unregister_groups
+ described_class.register_feature_groups
+ end
+
+ it 'registers expected groups' do
+ expect(Flipper.groups).to include(an_object_having_attributes(name: :gitlab_team_members))
+ end
+ end
+
describe '.enabled?' do
before do
allow(Feature).to receive(:log_feature_flag_states?).and_return(false)
@@ -350,6 +361,22 @@ RSpec.describe Feature, stub_feature_flags: false do
end
end
+ context 'with gitlab_team_members feature group' do
+ let(:actor) { build_stubbed(:user) }
+
+ before do
+ Flipper.unregister_groups
+ described_class.register_feature_groups
+ described_class.enable(:enabled_feature_flag, :gitlab_team_members)
+ end
+
+ it 'delegates check to FeatureGroups::GitlabTeamMembers' do
+ expect(FeatureGroups::GitlabTeamMembers).to receive(:enabled?).with(actor)
+
+ described_class.enabled?(:enabled_feature_flag, actor)
+ 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') }
diff --git a/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
new file mode 100644
index 00000000000..7c7ca8207ff
--- /dev/null
+++ b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'active_support/testing/stream'
+
+RSpec.describe Gitlab::Partitioning::ForeignKeysGenerator, :migration, :silence_stdout,
+feature_category: :continuous_integration do
+ include ActiveSupport::Testing::Stream
+ include MigrationsHelpers
+
+ before do
+ ActiveRecord::Schema.define do
+ create_table :_test_tmp_builds, force: :cascade do |t|
+ t.integer :partition_id
+ t.index [:id, :partition_id], unique: true
+ end
+
+ create_table :_test_tmp_metadata, force: :cascade do |t|
+ t.integer :partition_id
+ t.references :builds, foreign_key: { to_table: :_test_tmp_builds, on_delete: :cascade }
+ end
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(destination_root)
+
+ table(:schema_migrations).where(version: migrations.map(&:version)).delete_all
+
+ active_record_base.connection.execute(<<~SQL)
+ DROP TABLE _test_tmp_metadata;
+ DROP TABLE _test_tmp_builds;
+ SQL
+ end
+
+ let_it_be(:destination_root) { File.expand_path("../tmp", __dir__) }
+
+ let(:generator_config) { { destination_root: destination_root } }
+ let(:generator_args) { ['--source', '_test_tmp_metadata', '--target', '_test_tmp_builds', '--database', 'main'] }
+
+ context 'without foreign keys' do
+ let(:generator_args) { ['--source', '_test_tmp_metadata', '--target', 'projects', '--database', 'main'] }
+
+ it 'does not generate migrations' do
+ output = capture(:stderr) { run_generator }
+
+ expect(migrations).to be_empty
+ expect(output).to match(/No FK found between _test_tmp_metadata and projects/)
+ end
+ end
+
+ context 'with one FK' do
+ it 'generates foreign key migrations' do
+ run_generator
+
+ expect(migrations.sort_by(&:version).map(&:name)).to eq(%w[
+ AddFkIndexToTestTmpMetadataOnPartitionIdAndBuildsId
+ AddFkToTestTmpMetadataOnPartitionIdAndBuildsId
+ ValidateFkOnTestTmpMetadataPartitionIdAndBuildsId
+ RemoveFkToTestTmpBuildsTestTmpMetadataOnBuildsId
+ ])
+
+ schema_migrate_up!
+
+ fks = Gitlab::Database::PostgresForeignKey
+ .by_referenced_table_identifier('public._test_tmp_builds')
+ .by_constrained_table_identifier('public._test_tmp_metadata')
+
+ expect(fks.size).to eq(1)
+
+ foreign_key = fks.first
+
+ expect(foreign_key.name).to end_with('_p')
+ expect(foreign_key.constrained_columns).to eq(%w[partition_id builds_id])
+ expect(foreign_key.referenced_columns).to eq(%w[partition_id id])
+ expect(foreign_key.on_delete_action).to eq('cascade')
+ expect(foreign_key.on_update_action).to eq('cascade')
+
+ index = active_record_base.connection.indexes('_test_tmp_metadata').find do |index|
+ index.columns == %w[partition_id builds_id]
+ end
+
+ expect(index).to be_present
+ end
+ end
+
+ context 'with many FKs' do
+ before do
+ ActiveRecord::Schema.define do
+ add_reference :_test_tmp_metadata, :job,
+ foreign_key: { to_table: :_test_tmp_builds, on_delete: :cascade }
+ end
+ end
+
+ it 'generates migrations for the selected FK' do
+ expect(Thor::LineEditor)
+ .to receive(:readline)
+ .with('Please select one: [0, 1] (0) ', { default: '0', limited_to: %w[0 1] })
+ .and_return('1')
+
+ run_generator
+
+ expect(migrations.sort_by(&:version).map(&:name)).to eq(%w[
+ AddFkIndexToTestTmpMetadataOnPartitionIdAndJobId
+ AddFkToTestTmpMetadataOnPartitionIdAndJobId
+ ValidateFkOnTestTmpMetadataPartitionIdAndJobId
+ RemoveFkToTestTmpBuildsTestTmpMetadataOnJobId
+ ])
+ end
+ end
+
+ def run_generator(args = generator_args, config = generator_config)
+ described_class.start(args, config)
+ end
+
+ # We want to execute only the newly generated migrations
+ def migrations_paths
+ [File.join(destination_root, 'db', 'post_migrate')]
+ end
+
+ # There is no need to migrate down before executing the tests because these
+ # migrations were not already executed and we don't need to run it after
+ # the tests because we're removing the tables.
+ def schema_migrate_down!
+ # no-op
+ end
+end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
index bf2f8d8159b..bd6af4269b4 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/base_query_builder_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::BaseQueryBuilder d
let_it_be(:issue_outside_project) { create(:issue) }
let_it_be(:stage) do
- create(:cycle_analytics_project_stage,
+ create(:cycle_analytics_stage,
project: project,
start_event_identifier: :issue_created,
end_event_identifier: :issue_deployed_to_production
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
index 7e36d89a2a1..aa0a1b66eef 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
let_it_be(:issue_2) { create(:issue, project: project) }
let_it_be(:issue_3) { create(:issue, project: project) }
- let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
+ let_it_be(:stage) { create(:cycle_analytics_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, namespace: project.reload.project_namespace) }
let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, stage_event_hash_id: stage.stage_event_hash_id, project_id: project.id, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
index e2fdd4918d5..de325454b34 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/average_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Average do
let(:stage) do
build(
- :cycle_analytics_project_stage,
+ :cycle_analytics_stage,
start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier,
end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::IssueFirstMentionedInCommit.identifier,
project: project
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 271022e7c55..59b2bacea50 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
@@ -10,10 +10,10 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do
let(:params) { { current_user: user } }
let(:records) do
- stage = build(:cycle_analytics_project_stage, {
+ stage = build(:cycle_analytics_stage, {
start_event_identifier: :merge_request_created,
end_event_identifier: :merge_request_merged,
- project: project
+ namespace: project.reload.project_namespace
})
described_class.new(stage: stage, params: params).build.to_a
end
@@ -25,6 +25,14 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder do
freeze_time
end
+ context 'when an unknown parent class is given' do
+ it 'raises error' do
+ stage = instance_double('Analytics::CycleAnalytics::Stage', parent: Issue.new)
+
+ expect { described_class.new(stage: stage) }.to raise_error(/unknown parent_class: Issue/)
+ end
+ end
+
describe 'date range parameters' do
context 'when filters by only the `from` parameter' do
before do
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
index 4db5d64164e..39cfab49f54 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
@@ -9,10 +9,10 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Median do
let(:stage) do
build(
- :cycle_analytics_project_stage,
+ :cycle_analytics_stage,
start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier,
end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier,
- project: project
+ namespace: project.reload.project_namespace
)
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 7f70a4cfc4e..e9a9dfeca82 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
let_it_be(:issue2) { create(:issue, project: project, confidential: true) }
let(:stage) do
- build(:cycle_analytics_project_stage, {
+ build(:cycle_analytics_stage, {
start_event_identifier: :plan_stage_start,
end_event_identifier: :issue_first_mentioned_in_commit,
project: project
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
let(:mr1) { create(:merge_request, created_at: 5.days.ago, source_project: project, allow_broken: true) }
let(:mr2) { create(:merge_request, created_at: 4.days.ago, source_project: project, allow_broken: true) }
let(:stage) do
- build(:cycle_analytics_project_stage, {
+ build(:cycle_analytics_stage, {
start_event_identifier: :merge_request_created,
end_event_identifier: :merge_request_merged,
project: project
@@ -110,10 +110,10 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
let_it_be(:issue3) { create(:issue, project: project) }
let(:stage) do
- build(:cycle_analytics_project_stage, {
+ build(:cycle_analytics_stage, {
start_event_identifier: :plan_stage_start,
end_event_identifier: :issue_first_mentioned_in_commit,
- project: project
+ namespace: project.project_namespace
})
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb
index daf85ea379a..d0ada3d195b 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/sorting_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Sorting do
- let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
+ let(:stage) { build(:cycle_analytics_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:order_values) { described_class.new(query: MergeRequest.joins(:metrics), stage: stage).apply(sort, direction).order_values }
diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
index 9f86b95651a..c0c8e7aba63 100644
--- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::APIAuthentication::TokenResolver do
+RSpec.describe Gitlab::APIAuthentication::TokenResolver, feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
@@ -115,9 +115,9 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an unauthorized request'
end
- context 'when the external_authorization_service is enabled' do
+ context 'when the the deploy token is restricted with external_authorization' do
before do
- stub_application_setting(external_authorization_service_enabled: true)
+ allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false)
end
context 'with a valid deploy token' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 3d9076c6fa7..cb9d1e9eae8 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -855,8 +855,8 @@ module Gitlab
end
end
- def render(*args)
- described_class.render(*args)
+ def render(...)
+ described_class.render(...)
end
end
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 484b4702497..6aedd0a0a23 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Auth::AuthFinders do
+RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :authentication_and_authorization do
include described_class
include HttpBasicAuthHelpers
@@ -390,9 +390,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
end
- context 'when the external_authorization_service is enabled' do
+ context 'when the the deploy token is restricted with external_authorization' do
before do
- stub_application_setting(external_authorization_service_enabled: true)
+ allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false)
set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
end
@@ -470,7 +470,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
- context 'no feed, API or archive requests' do
+ context 'no feed, API, archive or download requests' do
it 'returns nil if the request is not RSS' do
expect(find_user_from_web_access_token(:rss)).to be_nil
end
@@ -486,6 +486,10 @@ RSpec.describe Gitlab::Auth::AuthFinders do
it 'returns nil if the request is not ARCHIVE' do
expect(find_user_from_web_access_token(:archive)).to be_nil
end
+
+ it 'returns nil if the request is not DOWNLOAD' do
+ expect(find_user_from_web_access_token(:download)).to be_nil
+ end
end
it 'returns the user for RSS requests' do
@@ -506,6 +510,12 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(find_user_from_web_access_token(:archive)).to eq(user)
end
+ it 'returns the user for DOWNLOAD requests' do
+ set_header('SCRIPT_NAME', '/-/1.0.0/downloads/main.zip')
+
+ expect(find_user_from_web_access_token(:download)).to eq(user)
+ end
+
context 'for API requests' do
it 'returns the user' do
set_header('SCRIPT_NAME', '/api/endpoint')
diff --git a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
index 3d9be4c3489..c5703cf5c24 100644
--- a/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/ip_rate_limiter_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Auth::IpRateLimiter, :use_clean_rails_memory_store_cachin
before do
stub_rack_attack_setting(options)
- Rack::Attack.reset!
+ Gitlab::Redis::RateLimiting.with(&:flushdb)
Rack::Attack.clear_configuration
Gitlab::RackAttack.configure(Rack::Attack)
end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index beeb3ca7011..a5cbad74829 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -418,25 +418,54 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :authentication_and_
end
context "and LDAP user has an account already" do
- let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
+ context 'when sync_name is disabled' do
+ before do
+ allow(Gitlab.config.ldap).to receive(:sync_name).and_return(false)
+ end
- it "adds the omniauth identity to the LDAP account" do
- allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
+ let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
- oauth_user.save # rubocop:disable Rails/SaveBang
+ it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
- expect(gl_user).to be_valid
- expect(gl_user.username).to eql 'john'
- expect(gl_user.name).to eql 'John Doe'
- expect(gl_user.email).to eql 'john@example.com'
- expect(gl_user.identities.length).to be 2
- identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
- expect(identities_as_hash).to match_array(
- [
- { provider: 'ldapmain', extern_uid: dn },
- { provider: 'twitter', extern_uid: uid }
- ]
- )
+ oauth_user.save # rubocop:disable Rails/SaveBang
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.name).to eql 'John Doe'
+ expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
+
+ context 'when sync_name is enabled' do
+ let!(:existing_user) { create(:omniauth_user, name: 'John Swift', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
+
+ it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user)
+
+ oauth_user.save # rubocop:disable Rails/SaveBang
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.name).to eql 'John Swift'
+ expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 5a6fa7c416b..a5f46aa1f35 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
+RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_category: :authentication_and_authorization do
let_it_be(:project) { create(:project) }
let(:auth_failure) { { actor: nil, project: nil, type: nil, authentication_abilities: nil } }
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'ADMIN_SCOPES contains all scopes for ADMIN access' do
- expect(subject::ADMIN_SCOPES).to match_array %i[sudo]
+ expect(subject::ADMIN_SCOPES).to match_array %i[sudo admin_mode]
end
it 'REPOSITORY_SCOPES contains all scopes for REPOSITORY access' do
@@ -28,19 +28,13 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
it 'DEFAULT_SCOPES contains all default scopes' do
expect(subject::DEFAULT_SCOPES).to match_array [:api]
end
-
- it 'optional_scopes contains all non-default scopes' do
- stub_container_registry_config(enabled: true)
-
- expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo openid profile email]
- end
end
context 'available_scopes' do
it 'contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
+ expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode]
end
it 'contains for non-admin user all non-default scopes without ADMIN access' do
@@ -54,7 +48,38 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
stub_container_registry_config(enabled: true)
user = create(:user, admin: true)
- expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode]
+ end
+
+ it 'optional_scopes contains all non-default scopes' do
+ stub_container_registry_config(enabled: true)
+
+ expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email]
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
+ it 'contains all non-default scopes' do
+ stub_container_registry_config(enabled: true)
+
+ expect(subject.all_available_scopes).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode]
+ end
+
+ it 'contains for admin user all non-default scopes with ADMIN access' do
+ stub_container_registry_config(enabled: true)
+ user = create(:user, admin: true)
+
+ expect(subject.available_scopes_for(user)).to match_array %i[api read_user read_api read_repository write_repository read_registry write_registry sudo]
+ end
+
+ it 'optional_scopes contains all non-default scopes' do
+ stub_container_registry_config(enabled: true)
+
+ expect(subject.optional_scopes).to match_array %i[read_user read_api read_repository write_repository read_registry write_registry sudo admin_mode openid profile email]
+ end
end
context 'registry_scopes' do
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
deleted file mode 100644
index 96adea03d43..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BackfillJiraTrackerDeploymentType2, :migration, schema: 20210602155110 do
- 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') }
-
- subject { described_class.new }
-
- def create_tracker_data(options = {})
- jira_tracker_data_temp.create!({ service_id: jira_integration.id }.merge(options))
- end
-
- describe '#perform' do
- context do
- it 'ignores if deployment already set' do
- tracker_data = create_tracker_data(url: atlassian_host, deployment_type: 'server')
-
- expect(subject).not_to receive(:collect_deployment_type)
-
- subject.perform(tracker_data.id, tracker_data.id)
-
- expect(tracker_data.reload.deployment_type).to eq 'server'
- end
-
- it 'ignores if no url is set' do
- tracker_data = create_tracker_data(deployment_type: 'unknown')
-
- expect(subject).to receive(:collect_deployment_type)
-
- subject.perform(tracker_data.id, tracker_data.id)
-
- expect(tracker_data.reload.deployment_type).to eq 'unknown'
- end
- end
-
- context 'when tracker is valid' do
- let!(:tracker_1) { create_tracker_data(url: atlassian_host, deployment_type: 0) }
- let!(:tracker_2) { create_tracker_data(url: mixedcase_host, deployment_type: 0) }
- let!(:tracker_3) { create_tracker_data(url: server_host, deployment_type: 0) }
- let!(:tracker_4) { create_tracker_data(api_url: server_host, deployment_type: 0) }
- let!(:tracker_nextbatch) { create_tracker_data(api_url: atlassian_host, deployment_type: 0) }
-
- it 'sets the proper deployment_type', :aggregate_failures do
- subject.perform(tracker_1.id, tracker_4.id)
-
- expect(tracker_1.reload.deployment_cloud?).to be_truthy
- expect(tracker_2.reload.deployment_cloud?).to be_truthy
- expect(tracker_3.reload.deployment_server?).to be_truthy
- expect(tracker_4.reload.deployment_server?).to be_truthy
- expect(tracker_nextbatch.reload.deployment_unknown?).to be_truthy
- end
- end
-
- it_behaves_like 'marks background migration job records' do
- let(:arguments) { [1, 4] }
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb
index 15956d2ea80..876eb070745 100644
--- a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsChildren, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsChildren, :migration, schema: 20210826171758 do
let(:namespaces_table) { table(:namespaces) }
let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) }
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb
index 019c6d54068..ad9b54608c6 100644
--- a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsRoots, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsRoots, :migration, schema: 20210826171758 do
let(:namespaces_table) { table(:namespaces) }
let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) }
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index 8d5aa6236a7..80fd86e90bb 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20210826171758,
+feature_category: :source_code_management do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:users) { table(:users) }
let(:snippets) { table(:snippets) }
diff --git a/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb
index b084e3fe885..7142aea3ab2 100644
--- a/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_upvotes_count_on_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillUpvotesCountOnIssues, schema: 20210701111909 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillUpvotesCountOnIssues, schema: 20210826171758 do
let(:award_emoji) { table(:award_emoji) }
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb b/spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb
index 0d9d9eb929c..5ffe665f0ad 100644
--- a/spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb
+++ b/spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::CleanupOrphanedLfsObjectsProjects, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::CleanupOrphanedLfsObjectsProjects, schema: 20210826171758 do
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:lfs_objects) { table(:lfs_objects) }
let(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb
index c4039b85459..8f058c875a2 100644
--- a/spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_deployments_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migration, schema: 20210617161348 do
+RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migration, schema: 20210826171758 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) }
@@ -10,17 +10,14 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migratio
before do
create_deployment!(environment.id, project.id)
- create_deployment!(non_existing_record_id, project.id)
end
it 'deletes only orphaned deployments' do
expect(valid_deployments.pluck(:id)).not_to be_empty
- expect(orphaned_deployments.pluck(:id)).not_to be_empty
subject.perform(table(:deployments).minimum(:id), table(:deployments).maximum(:id))
expect(valid_deployments.pluck(:id)).not_to be_empty
- expect(orphaned_deployments.pluck(:id)).to be_empty
end
it 'marks jobs as done' do
@@ -29,15 +26,9 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migratio
arguments: [table(:deployments).minimum(:id), table(:deployments).minimum(:id)]
)
- second_job = background_migration_jobs.create!(
- class_name: 'DeleteOrphanedDeployments',
- arguments: [table(:deployments).maximum(:id), table(:deployments).maximum(:id)]
- )
-
subject.perform(table(:deployments).minimum(:id), table(:deployments).minimum(:id))
expect(first_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:succeeded])
- expect(second_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:pending])
end
private
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 66e16b16270..8f3ef44e00c 100644
--- a/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/drop_invalid_vulnerabilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::DropInvalidVulnerabilities, schema: 20210826171758 do
let!(:background_migration_jobs) { table(:background_migration_jobs) }
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb
new file mode 100644
index 00000000000..b52f30a5e21
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::EncryptCiTriggerToken, feature_category: :continuous_integration do
+ let(:ci_triggers) do
+ table(:ci_triggers, database: :ci) do |ci_trigger|
+ ci_trigger.send :attr_encrypted, :encrypted_token_tmp,
+ attribute: :encrypted_token,
+ mode: :per_attribute_iv,
+ key: ::Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+ end
+ end
+
+ let(:without_encryption) { ci_triggers.create!(token: "token", owner_id: 1) }
+ let(:without_encryption_2) { ci_triggers.create!(token: "token 2", owner_id: 1) }
+ let(:with_encryption) { ci_triggers.create!(token: 'token 3', owner_id: 1, encrypted_token_tmp: 'token 3') }
+
+ let(:start_id) { ci_triggers.minimum(:id) }
+ let(:end_id) { ci_triggers.maximum(:id) }
+
+ let(:migration_attrs) do
+ {
+ start_id: start_id,
+ end_id: end_id,
+ batch_table: :ci_triggers,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: Ci::ApplicationRecord.connection
+ }
+ end
+
+ it 'ensures all unencrypted tokens are encrypted' do
+ expect(without_encryption.encrypted_token).to eq(nil)
+ expect(without_encryption_2.encrypted_token).to eq(nil)
+ expect(with_encryption.encrypted_token).not_to be(nil)
+
+ described_class.new(**migration_attrs).perform
+
+ updated_triggers = [without_encryption, without_encryption_2]
+ updated_triggers.each do |stale_trigger|
+ db_trigger = Ci::Trigger.find(stale_trigger.id)
+ expect(db_trigger.encrypted_token).not_to be(nil)
+ expect(db_trigger.encrypted_token_iv).not_to be(nil)
+ expect(db_trigger.token).to eq(db_trigger.encrypted_token_tmp)
+ end
+
+ already_encrypted_token = Ci::Trigger.find(with_encryption.id)
+ expect(already_encrypted_token.encrypted_token).to eq(with_encryption.encrypted_token)
+ expect(already_encrypted_token.encrypted_token_iv).to eq(with_encryption.encrypted_token_iv)
+ expect(with_encryption.token).to eq(with_encryption.encrypted_token_tmp)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb b/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb
index 51a09d50a19..586e75ffb37 100644
--- a/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb
+++ b/spec/lib/gitlab/background_migration/extract_project_topics_into_separate_table_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::ExtractProjectTopicsIntoSeparateTable,
- :suppress_gitlab_schemas_validate_connection, schema: 20210730104800 do
+ :suppress_gitlab_schemas_validate_connection, schema: 20210826171758 do
it 'correctly extracts project topics into separate table' do
namespaces = table(:namespaces)
projects = table(:projects)
diff --git a/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
new file mode 100644
index 00000000000..f71b54a7eb4
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_incoherent_packages_size_on_project_statistics_spec.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop: disable RSpec/MultipleMemoizedHelpers
+RSpec.describe Gitlab::BackgroundMigration::FixIncoherentPackagesSizeOnProjectStatistics,
+ feature_category: :package_registry do
+ let(:project_statistics_table) { table(:project_statistics) }
+ let(:packages_table) { table(:packages_packages) }
+ let(:package_files_table) { table(:packages_package_files) }
+ let(:projects_table) { table(:projects) }
+ let(:namespaces_table) { table(:namespaces) }
+
+ let!(:group) { namespaces_table.create!(name: 'group', path: 'group', type: 'Group') }
+
+ let!(:project_1_namespace) do
+ namespaces_table.create!(name: 'project1', path: 'project1', type: 'Project', parent_id: group.id)
+ end
+
+ let!(:project_2_namespace) do
+ namespaces_table.create!(name: 'project2', path: 'project2', type: 'Project', parent_id: group.id)
+ end
+
+ let!(:project_3_namespace) do
+ namespaces_table.create!(name: 'project3', path: 'project3', type: 'Project', parent_id: group.id)
+ end
+
+ let!(:project_4_namespace) do
+ namespaces_table.create!(name: 'project4', path: 'project4', type: 'Project', parent_id: group.id)
+ end
+
+ let!(:project_1) do
+ projects_table.create!(
+ namespace_id: group.id,
+ name: 'project1',
+ path: 'project1',
+ project_namespace_id: project_1_namespace.id
+ )
+ end
+
+ let!(:project_2) do
+ projects_table.create!(
+ namespace_id: group.id,
+ name: 'project2',
+ path: 'project2',
+ project_namespace_id: project_2_namespace.id
+ )
+ end
+
+ let!(:project_3) do
+ projects_table.create!(
+ namespace_id: group.id,
+ name: 'project3',
+ path: 'project3',
+ project_namespace_id: project_3_namespace.id
+ )
+ end
+
+ let!(:project_4) do
+ projects_table.create!(
+ namespace_id: group.id,
+ name: 'project4',
+ path: 'project4',
+ project_namespace_id: project_4_namespace.id
+ )
+ end
+
+ let!(:coherent_non_zero_statistics) do
+ project_statistics_table.create!(namespace_id: group.id, project_id: project_1.id, packages_size: 200)
+ end
+
+ let!(:incoherent_non_zero_statistics) do
+ project_statistics_table.create!(namespace_id: group.id, project_id: project_2.id, packages_size: 5)
+ end
+
+ let!(:coherent_zero_statistics) do
+ project_statistics_table.create!(namespace_id: group.id, project_id: project_4.id, packages_size: 0)
+ end
+
+ let!(:incoherent_zero_statistics) do
+ project_statistics_table.create!(namespace_id: group.id, project_id: project_3.id, packages_size: 0)
+ end
+
+ let!(:package_1) do
+ packages_table.create!(project_id: project_1.id, name: 'test1', version: '1.2.3', package_type: 2)
+ end
+
+ let!(:package_2) do
+ packages_table.create!(project_id: project_2.id, name: 'test2', version: '1.2.3', package_type: 2)
+ end
+
+ let!(:package_3) do
+ packages_table.create!(project_id: project_2.id, name: 'test3', version: '1.2.3', package_type: 2)
+ end
+
+ let!(:package_4) do
+ packages_table.create!(project_id: project_3.id, name: 'test4', version: '1.2.3', package_type: 2)
+ end
+
+ let!(:package_5) do
+ packages_table.create!(project_id: project_3.id, name: 'test5', version: '1.2.3', package_type: 2)
+ end
+
+ let!(:package_file_1_1) do
+ package_files_table.create!(package_id: package_1.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let!(:package_file_1_2) do
+ package_files_table.create!(package_id: package_1.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let!(:package_file_2_1) do
+ package_files_table.create!(package_id: package_2.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let!(:package_file_3_1) do
+ package_files_table.create!(package_id: package_3.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let!(:package_file_4_1) do
+ package_files_table.create!(package_id: package_4.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let!(:package_file_5_1) do
+ package_files_table.create!(package_id: package_5.id, file_name: 'test.txt', file: 'test', size: 100)
+ end
+
+ let(:migration) do
+ described_class.new(
+ start_id: project_statistics_table.minimum(:id),
+ end_id: project_statistics_table.maximum(:id),
+ batch_table: :project_statistics,
+ batch_column: :id,
+ sub_batch_size: 1000,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ describe '#filter_batch' do
+ it 'selects all package size statistics' do
+ expected = project_statistics_table.pluck(:id)
+ actual = migration.filter_batch(project_statistics_table).pluck(:id)
+
+ expect(actual).to match_array(expected)
+ end
+ end
+
+ describe '#perform', :aggregate_failures, :clean_gitlab_redis_cache do
+ subject(:perform) { migration.perform }
+
+ shared_examples 'not updating project statistics' do
+ it 'does not change them' do
+ expect(FlushCounterIncrementsWorker).not_to receive(:perform_in)
+ expect { perform }
+ .to not_change { incoherent_non_zero_statistics.reload.packages_size }
+ .and not_change { coherent_non_zero_statistics.reload.packages_size }
+ .and not_change { incoherent_zero_statistics.reload.packages_size }
+ .and not_change { coherent_zero_statistics.reload.packages_size }
+ expect_buffered_update(incoherent_non_zero_statistics, 0)
+ expect_buffered_update(incoherent_zero_statistics, 0)
+ end
+ end
+
+ shared_examples 'enqueuing a buffered updates' do |updates|
+ it 'fixes the packages_size stat' do
+ updates_for_stats = updates.deep_transform_keys { |k| public_send(k) }
+ updates_for_stats.each do |stat, amount|
+ expect(FlushCounterIncrementsWorker)
+ .to receive(:perform_in).with(
+ ::Gitlab::Counters::BufferedCounter::WORKER_DELAY,
+ 'ProjectStatistics',
+ stat.id,
+ :packages_size
+ )
+
+ expect(::Gitlab::BackgroundMigration::Logger)
+ .to receive(:info).with(
+ migrator: described_class::MIGRATOR,
+ project_id: stat.project_id,
+ old_size: stat.packages_size,
+ new_size: stat.packages_size + amount
+ )
+ end
+
+ expect { perform }
+ .to not_change { incoherent_non_zero_statistics.reload.packages_size }
+ .and not_change { coherent_non_zero_statistics.reload.packages_size }
+ .and not_change { incoherent_zero_statistics.reload.packages_size }
+ .and not_change { coherent_zero_statistics.reload.packages_size }
+
+ updates_for_stats.each do |stat, amount|
+ expect_buffered_update(stat, amount)
+ end
+ end
+ end
+
+ context 'with incoherent packages_size' do
+ it_behaves_like 'enqueuing a buffered updates',
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
+
+ context 'with updates waiting on redis' do
+ before do
+ insert_packages_size_update(incoherent_non_zero_statistics, -50)
+ insert_packages_size_update(incoherent_zero_statistics, -50)
+ end
+
+ it_behaves_like 'enqueuing a buffered updates',
+ incoherent_non_zero_statistics: 195,
+ incoherent_zero_statistics: 200
+ end
+ end
+
+ context 'with no incoherent packages_size' do
+ before do
+ incoherent_non_zero_statistics.update!(packages_size: 200)
+ incoherent_zero_statistics.update!(packages_size: 200)
+ end
+
+ it_behaves_like 'not updating project statistics'
+ end
+
+ def insert_packages_size_update(stat, amount)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_key(stat), amount)
+ end
+ end
+
+ def expect_buffered_update(stat, expected)
+ amount = Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_key(stat)).to_i
+ end
+ expect(amount).to eq(expected)
+ end
+
+ def redis_key(stats)
+ "project:{#{stats.project_id}}:counters:ProjectStatistics:#{stats.id}:packages_size"
+ end
+ end
+end
+# rubocop: enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb b/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb
index 4d7c836cff4..b252df4ecff 100644
--- a/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigrateProjectTaggingsContextFromTagsToTopics,
- :suppress_gitlab_schemas_validate_connection, schema: 20210602155110 do
+ :suppress_gitlab_schemas_validate_connection, schema: 20210826171758 do
it 'correctly migrates project taggings context from tags to topics' do
taggings = table(:taggings)
diff --git a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
index fe45eaac3b7..08fde0d0ff4 100644
--- a/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_u2f_webauthn_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'webauthn/u2f_migrator'
-RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::MigrateU2fWebauthn, :migration, schema: 20210826171758 do
let(:users) { table(:users) }
let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
diff --git a/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb b/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
index cafddb6aeaf..71cf58a933f 100644
--- a/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
+++ b/spec/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::MoveContainerRegistryEnabledToProjectFeature, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::MoveContainerRegistryEnabledToProjectFeature, :migration, schema: 20210826171758 do
let(:enabled) { 20 }
let(:disabled) { 0 }
diff --git a/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
new file mode 100644
index 00000000000..a8574411957
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/nullify_creator_id_column_of_orphaned_projects_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::NullifyCreatorIdColumnOfOrphanedProjects, feature_category: :projects do
+ let(:users) { table(:users) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+
+ let(:user_1) { users.create!(name: 'user_1', email: 'user_1@example.com', projects_limit: 4) }
+ let(:user_2) { users.create!(name: 'user_2', email: 'user_2@example.com', projects_limit: 4) }
+ let(:user_3) { users.create!(name: 'user_3', email: 'user_3@example.com', projects_limit: 4) }
+
+ let!(:group) do
+ namespaces.create!(
+ name: 'Group1', type: 'Group', path: 'space1'
+ )
+ end
+
+ let!(:project_namespace_1) do
+ namespaces.create!(
+ name: 'project_1', path: 'project_1', type: 'Project'
+ )
+ end
+
+ let!(:project_namespace_2) do
+ namespaces.create!(
+ name: 'project_2', path: 'project_2', type: 'Project'
+ )
+ end
+
+ let!(:project_namespace_3) do
+ namespaces.create!(
+ name: 'project_3', path: 'project_3', type: 'Project'
+ )
+ end
+
+ let!(:project_namespace_4) do
+ namespaces.create!(
+ name: 'project_4', path: 'project_4', type: 'Project'
+ )
+ end
+
+ let!(:project_1) do
+ projects.create!(
+ name: 'project_1', path: 'project_1', namespace_id: group.id, project_namespace_id: project_namespace_1.id,
+ creator_id: user_1.id
+ )
+ end
+
+ let!(:project_2) do
+ projects.create!(
+ name: 'project_2', path: 'project_2', namespace_id: group.id, project_namespace_id: project_namespace_2.id,
+ creator_id: user_2.id
+ )
+ end
+
+ let!(:project_3) do
+ projects.create!(
+ name: 'project_3', path: 'project_3', namespace_id: group.id, project_namespace_id: project_namespace_3.id,
+ creator_id: user_3.id
+ )
+ end
+
+ let!(:project_4) do
+ projects.create!(
+ name: 'project_4', path: 'project_4', namespace_id: group.id, project_namespace_id: project_namespace_4.id,
+ creator_id: nil
+ )
+ end
+
+ subject do
+ described_class.new(
+ start_id: project_1.id,
+ end_id: project_4.id,
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ ).perform
+ end
+
+ it 'nullifies the `creator_id` column of projects whose creators do not exist' do
+ # `delete` `user_3` so that the creator of `project_3` is removed, without invoking `dependent: :nullify` on `User`
+ user_3.delete
+
+ expect { subject }.to change { projects.where(creator_id: nil).count }.from(1).to(2)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/rebalance_partition_id_spec.rb b/spec/lib/gitlab/background_migration/rebalance_partition_id_spec.rb
new file mode 100644
index 00000000000..195e57e4e59
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/rebalance_partition_id_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RebalancePartitionId,
+ :migration,
+ schema: 20230125093723,
+ feature_category: :continuous_integration do
+ let(:ci_builds_table) { table(:ci_builds, database: :ci) }
+ let(:ci_pipelines_table) { table(:ci_pipelines, database: :ci) }
+
+ let!(:valid_ci_pipeline) { ci_pipelines_table.create!(id: 1, partition_id: 100) }
+ let!(:invalid_ci_pipeline) { ci_pipelines_table.create!(id: 2, partition_id: 101) }
+
+ describe '#perform' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:table_name, :invalid_record, :valid_record) do
+ :ci_pipelines | invalid_ci_pipeline | valid_ci_pipeline
+ end
+
+ subject(:perform) do
+ described_class.new(
+ start_id: 1,
+ end_id: 2,
+ batch_table: table_name,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: Ci::ApplicationRecord.connection
+ ).perform
+ end
+
+ shared_examples 'fix invalid records' do
+ it 'rebalances partition_id to 100 when partition_id is 101' do
+ expect { perform }
+ .to change { invalid_record.reload.partition_id }.from(101).to(100)
+ .and not_change { valid_record.reload.partition_id }
+ end
+ end
+
+ with_them do
+ it_behaves_like 'fix invalid records'
+ 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
deleted file mode 100644
index a19a3760958..00000000000
--- a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::SanitizeConfidentialTodos, :migration, feature_category: :team_planning do
- let!(:issue_type_id) { table(:work_item_types).find_by(base_type: 0).id }
-
- let(:todos) { table(:todos) }
- let(:notes) { table(:notes) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_features) { table(:project_features) }
- let(:users) { table(:users) }
- let(:issues) { table(:issues) }
- let(:members) { table(:members) }
- let(:project_authorizations) { table(:project_authorizations) }
-
- let(:user) { users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1) }
- let(:project_namespace1) { namespaces.create!(path: 'pns1', name: 'pns1') }
- let(:project_namespace2) { namespaces.create!(path: 'pns2', name: 'pns2') }
-
- let(:project1) do
- projects.create!(namespace_id: project_namespace1.id,
- project_namespace_id: project_namespace1.id, visibility_level: 20)
- end
-
- let(:project2) do
- projects.create!(namespace_id: project_namespace2.id,
- project_namespace_id: project_namespace2.id)
- end
-
- let(:issue1) do
- issues.create!(
- project_id: project1.id, namespace_id: project_namespace1.id, issue_type: 1, title: 'issue1', author_id: user.id,
- work_item_type_id: issue_type_id
- )
- end
-
- let(:issue2) do
- issues.create!(
- project_id: project2.id, namespace_id: project_namespace2.id, issue_type: 1, title: 'issue2',
- work_item_type_id: issue_type_id
- )
- end
-
- let(:public_note) { notes.create!(note: 'text', project_id: project1.id) }
-
- let(:confidential_note) do
- notes.create!(note: 'text', project_id: project1.id, confidential: true,
- noteable_id: issue1.id, noteable_type: 'Issue')
- end
-
- let(:other_confidential_note) do
- notes.create!(note: 'text', project_id: project2.id, confidential: true,
- noteable_id: issue2.id, noteable_type: 'Issue')
- end
-
- let(:common_params) { { user_id: user.id, author_id: user.id, action: 1, state: 'pending', target_type: 'Note' } }
- let!(:ignored_todo1) { todos.create!(**common_params) }
- let!(:ignored_todo2) { todos.create!(**common_params, target_id: public_note.id, note_id: public_note.id) }
- let!(:valid_todo) { todos.create!(**common_params, target_id: confidential_note.id, note_id: confidential_note.id) }
- let!(:invalid_todo) do
- todos.create!(**common_params, target_id: other_confidential_note.id, note_id: other_confidential_note.id)
- end
-
- describe '#perform' do
- before do
- project_features.create!(project_id: project1.id, issues_access_level: 20, pages_access_level: 20)
- members.create!(state: 0, source_id: project1.id, source_type: 'Project',
- type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0,
- member_namespace_id: project_namespace1.id)
- project_authorizations.create!(project_id: project1.id, user_id: user.id, access_level: 50)
- end
-
- subject(:perform) do
- described_class.new(
- start_id: notes.minimum(:id),
- end_id: notes.maximum(:id),
- batch_table: :notes,
- batch_column: :id,
- sub_batch_size: 1,
- pause_ms: 0,
- connection: ApplicationRecord.connection
- ).perform
- end
-
- it 'deletes todos where user can not read its note and logs deletion', :aggregate_failures do
- expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
- expect(logger).to receive(:info).with(
- hash_including(
- message: "#{described_class.name} deleting invalid todo",
- attributes: hash_including(invalid_todo.attributes.slice(:id, :user_id, :target_id, :target_type))
- )
- ).once
- end
-
- expect { perform }.to change(todos, :count).by(-1)
-
- expect(todos.all).to match_array([ignored_todo1, ignored_todo2, valid_todo])
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb b/spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb
index 7261758e010..b8c3bf8f3ac 100644
--- a/spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_timelogs_project_id_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::UpdateTimelogsProjectId, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::UpdateTimelogsProjectId, schema: 20210826171758 do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
let!(:project1) { table(:projects).create!(namespace_id: namespace.id) }
let!(:project2) { table(:projects).create!(namespace_id: namespace.id) }
diff --git a/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb b/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb
index 4599491b580..f16ae489b78 100644
--- a/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::UpdateUsersWhereTwoFactorAuthRequiredFromGroup, :migration, schema: 20210602155110 do
+RSpec.describe Gitlab::BackgroundMigration::UpdateUsersWhereTwoFactorAuthRequiredFromGroup, :migration, schema: 20210826171758 do
include MigrationHelpers::NamespacesHelpers
let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true) }
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index f83ce01c617..1526a1a9f2d 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BitbucketImport::Importer do
+RSpec.describe Gitlab::BitbucketImport::Importer, feature_category: :integrations do
include ImportSpecHelper
before do
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index ab4be5a909a..3cff2411054 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BitbucketServerImport::Importer do
+RSpec.describe Gitlab::BitbucketServerImport::Importer, feature_category: :importers do
include ImportSpecHelper
let(:import_url) { 'http://my-bitbucket' }
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index 2dea0aef4cf..ec0bda3c300 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :repository) }
let(:pipeline_status) { described_class.new(project) }
diff --git a/spec/lib/gitlab/cache/helpers_spec.rb b/spec/lib/gitlab/cache/helpers_spec.rb
index 39d37e979b4..06131ee4546 100644
--- a/spec/lib/gitlab/cache/helpers_spec.rb
+++ b/spec/lib/gitlab/cache/helpers_spec.rb
@@ -33,10 +33,23 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do
context 'single object' do
let_it_be(:presentable) { create(:merge_request, source_project: project, source_branch: 'wip') }
- it_behaves_like 'object cache helper'
+ context 'when presenter is a serializer' do
+ let(:expected_cache_key_prefix) { 'MergeRequestSerializer' }
+
+ it_behaves_like 'object cache helper'
+ end
+
+ context 'when presenter is a Grape::Entity' do
+ let(:presenter) { API::Entities::MergeRequest }
+ let(:expected_cache_key_prefix) { 'API::Entities::MergeRequest' }
+
+ it_behaves_like 'object cache helper'
+ end
end
context 'collection of objects' do
+ let(:expected_cache_key_prefix) { 'MergeRequestSerializer' }
+
let_it_be(:presentable) do
[
create(:merge_request, source_project: project, source_branch: 'fix'),
@@ -46,5 +59,17 @@ RSpec.describe Gitlab::Cache::Helpers, :use_clean_rails_redis_caching do
it_behaves_like 'collection cache helper'
end
+
+ context 'when passed presenter is not a serializer or an entity' do
+ let(:presenter) { User }
+
+ let_it_be(:presentable) do
+ create(:merge_request, source_project: project, source_branch: 'master')
+ end
+
+ it 'throws an exception' do
+ expect { subject }.to raise_exception(ArgumentError, "presenter User is not supported")
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/cache/metadata_spec.rb b/spec/lib/gitlab/cache/metadata_spec.rb
new file mode 100644
index 00000000000..2e8af7a9c44
--- /dev/null
+++ b/spec/lib/gitlab/cache/metadata_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Cache::Metadata, feature_category: :source_code_management do
+ subject(:attributes) do
+ described_class.new(
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ )
+ end
+
+ let(:caller_id) { 'caller-id' }
+ let(:cache_identifier) { 'ApplicationController#show' }
+ let(:feature_category) { :source_code_management }
+ let(:backing_resource) { :unknown }
+
+ describe '#initialize' do
+ context 'when optional arguments are not set' do
+ before do
+ Gitlab::ApplicationContext.push(caller_id: 'context-id')
+ end
+
+ it 'sets default value for them' do
+ attributes = described_class.new(
+ cache_identifier: cache_identifier,
+ feature_category: feature_category
+ )
+
+ expect(attributes.backing_resource).to eq(:unknown)
+ expect(attributes.caller_id).to eq('context-id')
+ end
+ end
+
+ context 'when invalid feature category is set' do
+ let(:feature_category) { :not_supported }
+
+ it { expect { attributes }.to raise_error(RuntimeError) }
+
+ context 'when on production' do
+ before do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ end
+
+ it 'does not raise an exception' do
+ expect { attributes }.not_to raise_error
+ expect(attributes.feature_category).to eq('unknown')
+ end
+ end
+ end
+
+ context 'when backing resource is not supported' do
+ let(:backing_resource) { 'foo' }
+
+ it { expect { attributes }.to raise_error(RuntimeError) }
+
+ context 'when on production' do
+ before do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ end
+
+ it 'does not raise an exception' do
+ expect { attributes }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ describe '#caller_id' do
+ subject { attributes.caller_id }
+
+ it { is_expected.to eq caller_id }
+ end
+
+ describe '#cache_identifier' do
+ subject { attributes.cache_identifier }
+
+ it { is_expected.to eq cache_identifier }
+ end
+
+ describe '#feature_category' do
+ subject { attributes.feature_category }
+
+ it { is_expected.to eq feature_category }
+ end
+
+ describe '#backing_resource' do
+ subject { attributes.backing_resource }
+
+ it { is_expected.to eq backing_resource }
+ end
+end
diff --git a/spec/lib/gitlab/cache/metrics_spec.rb b/spec/lib/gitlab/cache/metrics_spec.rb
index d8103837708..24b274f4209 100644
--- a/spec/lib/gitlab/cache/metrics_spec.rb
+++ b/spec/lib/gitlab/cache/metrics_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
RSpec.describe Gitlab::Cache::Metrics do
- subject(:metrics) do
- described_class.new(
+ subject(:metrics) { described_class.new(metadata) }
+
+ let(:metadata) do
+ Gitlab::Cache::Metadata.new(
caller_id: caller_id,
cache_identifier: cache_identifier,
feature_category: feature_category,
@@ -27,24 +29,6 @@ RSpec.describe Gitlab::Cache::Metrics do
).and_return(counter_mock)
end
- describe '#initialize' do
- context 'when backing resource is not supported' do
- let(:backing_resource) { 'foo' }
-
- it { expect { metrics }.to raise_error(RuntimeError) }
-
- context 'when on production' do
- before do
- allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
- end
-
- it 'does not raise an exception' do
- expect { metrics }.not_to raise_error
- end
- end
- end
- end
-
describe '#increment_cache_hit' do
subject { metrics.increment_cache_hit }
diff --git a/spec/lib/gitlab/chat/responder_spec.rb b/spec/lib/gitlab/chat/responder_spec.rb
index 803f30da9e7..a9d290cb87c 100644
--- a/spec/lib/gitlab/chat/responder_spec.rb
+++ b/spec/lib/gitlab/chat/responder_spec.rb
@@ -2,30 +2,70 @@
require 'spec_helper'
-RSpec.describe Gitlab::Chat::Responder do
+RSpec.describe Gitlab::Chat::Responder, feature_category: :integrations do
describe '.responder_for' do
- context 'using a regular build' do
- it 'returns nil' do
- build = create(:ci_build)
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(use_response_url_for_chat_responder: false)
+ end
+
+ context 'using a regular build' do
+ it 'returns nil' do
+ build = create(:ci_build)
+
+ expect(described_class.responder_for(build)).to be_nil
+ end
+ end
+
+ context 'using a chat build' do
+ it 'returns the responder for the build' do
+ pipeline = create(:ci_pipeline)
+ build = create(:ci_build, pipeline: pipeline)
+ integration = double(:integration, chat_responder: Gitlab::Chat::Responder::Slack)
+ chat_name = double(:chat_name, integration: integration)
+ chat_data = double(:chat_data, chat_name: chat_name)
+
+ allow(pipeline)
+ .to receive(:chat_data)
+ .and_return(chat_data)
- expect(described_class.responder_for(build)).to be_nil
+ expect(described_class.responder_for(build))
+ .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
+ end
end
end
- context 'using a chat build' do
- it 'returns the responder for the build' do
- pipeline = create(:ci_pipeline)
- build = create(:ci_build, pipeline: pipeline)
- integration = double(:integration, chat_responder: Gitlab::Chat::Responder::Slack)
- chat_name = double(:chat_name, integration: integration)
- chat_data = double(:chat_data, chat_name: chat_name)
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(use_response_url_for_chat_responder: true)
+ end
+
+ context 'using a regular build' do
+ it 'returns nil' do
+ build = create(:ci_build)
+
+ expect(described_class.responder_for(build)).to be_nil
+ end
+ end
+
+ context 'using a chat build' do
+ let(:chat_name) { create(:chat_name, chat_id: 'U123') }
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline)
+ pipeline.create_chat_data!(
+ response_url: 'https://hooks.slack.com/services/12345',
+ chat_name_id: chat_name.id
+ )
+ pipeline
+ end
- allow(pipeline)
- .to receive(:chat_data)
- .and_return(chat_data)
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:responder) { described_class.new(build) }
- expect(described_class.responder_for(build))
- .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
+ it 'returns the responder for the build' do
+ expect(described_class.responder_for(build))
+ .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/artifacts/logger_spec.rb b/spec/lib/gitlab/ci/artifacts/logger_spec.rb
index 7753cb0d25e..7a2f8b6ea37 100644
--- a/spec/lib/gitlab/ci/artifacts/logger_spec.rb
+++ b/spec/lib/gitlab/ci/artifacts/logger_spec.rb
@@ -9,23 +9,27 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do
describe '.log_created' do
it 'logs information about created artifact' do
- artifact = create(:ci_job_artifact, :archive)
-
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Artifact created',
- job_artifact_id: artifact.id,
- size: artifact.size,
- type: artifact.file_type,
- build_id: artifact.job_id,
- project_id: artifact.project_id,
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
+ artifact_1 = create(:ci_job_artifact, :archive)
+ artifact_2 = create(:ci_job_artifact, :metadata)
+ artifacts = [artifact_1, artifact_2]
+
+ artifacts.each do |artifact|
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Artifact created',
+ job_artifact_id: artifact.id,
+ size: artifact.size,
+ file_type: artifact.file_type,
+ build_id: artifact.job_id,
+ project_id: artifact.project_id,
+ 'correlation_id' => an_instance_of(String),
+ 'meta.feature_category' => 'test',
+ 'meta.caller_id' => 'caller'
+ )
)
- )
+ end
- described_class.log_created(artifact)
+ described_class.log_created(artifacts)
end
end
@@ -43,7 +47,7 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do
job_artifact_id: artifact.id,
expire_at: artifact.expire_at,
size: artifact.size,
- type: artifact.file_type,
+ file_type: artifact.file_type,
build_id: artifact.job_id,
project_id: artifact.project_id,
method: method,
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index 9ff9200322e..314714c543b 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::AutoRetry do
+RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authoring do
let(:auto_retry) { described_class.new(build) }
describe '#allowed?' do
@@ -112,5 +112,13 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
expect(result).to eq ['always']
end
end
+
+ context 'with retry[:when] set to nil' do
+ let(:build) { create(:ci_build, options: { retry: { when: nil } }) }
+
+ it 'returns always array' do
+ expect(result).to eq ['always']
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb
index 31c7437cfe0..ebdb738f10b 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
+require 'spec_helper'
-RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If do
+RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If, feature_category: :continuous_integration do
include StubFeatureFlags
subject(:if_clause) { described_class.new(expression) }
diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb
new file mode 100644
index 00000000000..d9beae0555c
--- /dev/null
+++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do
+ let_it_be(:user) { create(:user) }
+
+ let(:path) { described_class.new(address: address, content_filename: 'template.yml') }
+ let(:settings) { Settingslogic.new({ 'component_fqdn' => current_host }) }
+ let(:current_host) { 'acme.com/' }
+
+ before do
+ allow(::Settings).to receive(:gitlab_ci).and_return(settings)
+ end
+
+ describe 'FQDN path' do
+ let_it_be(:existing_project) { create(:project, :repository) }
+
+ let(:project_path) { existing_project.full_path }
+ let(:address) { "acme.com/#{project_path}/component@#{version}" }
+ let(:version) { 'master' }
+
+ context 'when project exists' do
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+
+ context 'when content exists' do
+ let(:content) { 'image: alpine' }
+
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance)
+ .to receive(:blob_data_at)
+ .with(existing_project.commit('master').id, 'component/template.yml')
+ .and_return(content)
+ end
+ end
+
+ context 'when user has permissions to read code' do
+ before do
+ existing_project.add_developer(user)
+ end
+
+ it 'fetches the content' do
+ expect(path.fetch_content!(current_user: user)).to eq(content)
+ end
+ end
+
+ context 'when user does not have permissions to download code' do
+ it 'raises an error when fetching the content' do
+ expect { path.fetch_content!(current_user: user) }
+ .to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+ end
+
+ context 'when project path is nested under a subgroup' do
+ let(:existing_group) { create(:group, :nested) }
+ let(:existing_project) { create(:project, :repository, group: existing_group) }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+ end
+
+ context 'when current GitLab instance is installed on a relative URL' do
+ let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" }
+ let(:current_host) { 'acme.com/gitlab/' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to eq(existing_project.commit('master').id)
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+ end
+
+ context 'when version does not exist' do
+ let(:version) { 'non-existent' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to eq(existing_project)
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to be_nil
+ expect(path.project_file_path).to eq('component/template.yml')
+ end
+
+ it 'returns nil when fetching the content' do
+ expect(path.fetch_content!(current_user: user)).to be_nil
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:project_path) { 'non-existent/project' }
+
+ it 'provides the expected attributes', :aggregate_failures do
+ expect(path.project).to be_nil
+ expect(path.host).to eq(current_host)
+ expect(path.sha).to be_nil
+ expect(path.project_file_path).to be_nil
+ end
+
+ it 'returns nil when fetching the content' do
+ expect(path.fetch_content!(current_user: user)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb
index fd7f85c9298..5eecff5b592 100644
--- a/spec/lib/gitlab/ci/config/entry/include_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb
@@ -44,6 +44,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do
it { is_expected.to be_valid }
end
+ context 'when using "component"' do
+ let(:config) { { component: 'path/to/component@1.0' } }
+
+ it { is_expected.to be_valid }
+ end
+
context 'when using "artifact"' do
context 'and specifying "job"' do
let(:config) { { artifact: 'test.yml', job: 'generator' } }
diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb
index 40702e75404..1fd3cf3c99f 100644
--- a/spec/lib/gitlab/ci/config/external/context_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/context_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Context do
+RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_authoring do
let(:project) { build(:project) }
let(:user) { double('User') }
let(:sha) { '12345' }
@@ -14,7 +14,8 @@ RSpec.describe Gitlab::Ci::Config::External::Context do
describe 'attributes' do
context 'with values' do
it { is_expected.to have_attributes(**attributes) }
- it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.expandset).to eq([]) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) }
it { expect(subject.execution_deadline).to eq(0) }
it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
@@ -25,11 +26,39 @@ RSpec.describe Gitlab::Ci::Config::External::Context do
let(:attributes) { { project: nil, user: nil, sha: nil } }
it { is_expected.to have_attributes(**attributes) }
- it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.expandset).to eq([]) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) }
it { expect(subject.execution_deadline).to eq(0) }
it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
end
+
+ context 'when FF ci_includes_count_duplicates is disabled' do
+ before do
+ stub_feature_flags(ci_includes_count_duplicates: false)
+ end
+
+ context 'with values' do
+ it { is_expected.to have_attributes(**attributes) }
+ it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
+ it { expect(subject.execution_deadline).to eq(0) }
+ it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
+ it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
+ it { expect(subject.variables_hash).to include('a' => 'b') }
+ end
+
+ context 'without values' do
+ let(:attributes) { { project: nil, user: nil, sha: nil } }
+
+ it { is_expected.to have_attributes(**attributes) }
+ it { expect(subject.expandset).to eq(Set.new) }
+ it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) }
+ it { expect(subject.execution_deadline).to eq(0) }
+ it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) }
+ it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
+ end
+ end
end
describe '#set_deadline' do
diff --git a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
index a8dc7897082..45a15fb5f36 100644
--- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Artifact do
+RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_authoring do
let(:parent_pipeline) { create(:ci_pipeline) }
let(:variables) {}
let(:context) do
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact do
describe '#valid?' do
subject(:valid?) do
- external_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([external_file])
external_file.valid?
end
@@ -162,7 +162,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact do
user: anything
}
expect(context).to receive(:mutate).with(expected_attrs).and_call_original
- external_file.validate!
+
+ expect(valid?).to be_truthy
external_file.content
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 8475c3a8b19..55d95d0c1f8 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Base do
+RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_authoring do
let(:variables) {}
let(:context_params) { { sha: 'HEAD', variables: variables } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
@@ -51,7 +51,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base do
describe '#valid?' do
subject(:valid?) do
- file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([file])
file.valid?
end
diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
new file mode 100644
index 00000000000..a162a1a8abf
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do
+ let_it_be(:context_project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project_variables) { project.predefined_variables }
+
+ let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
+ let(:external_resource) { described_class.new(params, context) }
+ let(:params) { { component: 'gitlab.com/acme/components/my-component@1.0' } }
+ let(:fetch_service) { instance_double(::Ci::Components::FetchService) }
+ let(:response) { ServiceResponse.error(message: 'some error message') }
+
+ let(:context_params) do
+ {
+ project: context_project,
+ sha: '12345',
+ user: user,
+ variables: project_variables
+ }
+ end
+
+ before do
+ allow(::Ci::Components::FetchService)
+ .to receive(:new)
+ .with(
+ address: params[:component],
+ current_user: context.user
+ ).and_return(fetch_service)
+
+ allow(fetch_service).to receive(:execute).and_return(response)
+ end
+
+ describe '#matching?' do
+ subject(:matching) { external_resource.matching? }
+
+ context 'when component is specified' do
+ let(:params) { { component: 'some-value' } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when feature flag ci_include_components is disabled' do
+ before do
+ stub_feature_flags(ci_include_components: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when component is not specified' do
+ let(:params) { { local: 'some-value' } }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#valid?' do
+ subject(:valid?) do
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([external_resource])
+ external_resource.valid?
+ end
+
+ context 'when the context project does not have a repository' do
+ before do
+ allow(context_project).to receive(:repository).and_return(nil)
+ end
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Unable to use components outside of a project context')
+ end
+ end
+
+ context 'when location is not provided' do
+ let(:params) { { component: 123 } }
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Included file `123` needs to be a string')
+ end
+ end
+
+ context 'when component path is provided' do
+ context 'when component is not found' do
+ let(:response) do
+ ServiceResponse.error(message: 'Content not found')
+ end
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to eq('Content not found')
+ end
+ end
+
+ context 'when component is found' do
+ let(:content) do
+ <<~COMPONENT
+ job:
+ script: echo
+ COMPONENT
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: {
+ content: content,
+ path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345')
+ })
+ end
+
+ it 'is valid' do
+ expect(subject).to be_truthy
+ expect(external_resource.content).to eq(content)
+ end
+
+ context 'when content is not a valid YAML' do
+ let(:content) { 'the-content' }
+
+ it 'is invalid' do
+ expect(subject).to be_falsy
+ expect(external_resource.error_message).to match(/does not have valid YAML syntax/)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#metadata' do
+ subject(:metadata) { external_resource.metadata }
+
+ let(:component_path) do
+ instance_double(::Gitlab::Ci::Components::InstancePath,
+ project: project,
+ sha: '12345',
+ project_file_path: 'my-component/template.yml')
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: { path: component_path })
+ end
+
+ it 'returns the metadata' do
+ is_expected.to include(
+ context_project: context_project.full_path,
+ context_sha: context.sha,
+ type: :component,
+ location: 'gitlab.com/acme/components/my-component@1.0',
+ blob: a_string_ending_with("#{project.full_path}/-/blob/12345/my-component/template.yml"),
+ raw: nil,
+ extra: {}
+ )
+ end
+ end
+
+ describe '#expand_context' do
+ let(:component_path) do
+ instance_double(::Gitlab::Ci::Components::InstancePath,
+ project: project,
+ sha: '12345')
+ end
+
+ let(:response) do
+ ServiceResponse.success(payload: { path: component_path })
+ end
+
+ subject { external_resource.send(:expand_context_attrs) }
+
+ it 'inherits user and variables while changes project and sha' do
+ is_expected.to include(
+ project: project,
+ sha: '12345',
+ user: context.user,
+ variables: context.variables)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
index a77acb45978..b5895b4bc81 100644
--- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
@@ -30,6 +30,40 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
.to receive(:check_execution_time!)
end
+ describe '.initialize' do
+ context 'when a local is specified' do
+ let(:params) { { local: 'file' } }
+
+ it 'sets the location' do
+ expect(local_file.location).to eq('file')
+ end
+
+ context 'when the local is prefixed with a slash' do
+ let(:params) { { local: '/file' } }
+
+ it 'removes the slash' do
+ expect(local_file.location).to eq('file')
+ end
+ end
+
+ context 'when the local is prefixed with multiple slashes' do
+ let(:params) { { local: '//file' } }
+
+ it 'removes slashes' do
+ expect(local_file.location).to eq('file')
+ end
+ end
+ end
+
+ context 'with a missing local' do
+ let(:params) { { local: nil } }
+
+ it 'sets the location to an empty string' do
+ expect(local_file.location).to eq('')
+ end
+ end
+ end
+
describe '#matching?' do
context 'when a local is specified' do
let(:params) { { local: 'file' } }
@@ -58,7 +92,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
describe '#valid?' do
subject(:valid?) do
- local_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([local_file])
local_file.valid?
end
@@ -88,10 +122,13 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret', 'masked' => true }]) }
let(:location) { '/lib/gitlab/ci/templates/secret/existent-file.yml' }
- it 'returns false and adds an error message about an empty file' do
+ before do
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("")
- local_file.validate!
- expect(local_file.errors).to include("Local file `/lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!")
+ end
+
+ it 'returns false and adds an error message about an empty file' do
+ expect(valid?).to be_falsy
+ expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!")
end
end
@@ -101,7 +138,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
it 'returns false and adds an error message stating that included file does not exist' do
expect(valid?).to be_falsy
- expect(local_file.errors).to include("Sha #{sha} is not valid!")
+ expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/existent-file.yml` does not exist!")
end
end
end
@@ -143,11 +180,11 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) }
before do
- local_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([local_file])
end
it 'returns an error message' do
- expect(local_file.error_message).to eq("Local file `/lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!")
+ expect(local_file.error_message).to eq("Local file `lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!")
end
end
@@ -203,7 +240,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
context_project: project.full_path,
context_sha: sha,
type: :local,
- location: '/lib/gitlab/ci/templates/existent-file.yml',
+ location: 'lib/gitlab/ci/templates/existent-file.yml',
blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/lib/gitlab/ci/templates/existent-file.yml",
raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/lib/gitlab/ci/templates/existent-file.yml",
extra: {}
diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
index 0ba92d1e92d..abe38cdbc3e 100644
--- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Project do
+RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_authoring do
+ include RepoHelpers
+
let_it_be(:context_project) { create(:project) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
@@ -12,11 +14,12 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:project_file) { described_class.new(params, context) }
let(:variables) { project.predefined_variables.to_runner_variables }
+ let(:project_sha) { project.commit.sha }
let(:context_params) do
{
project: context_project,
- sha: '12345',
+ sha: project_sha,
user: context_user,
parent_pipeline: parent_pipeline,
variables: variables
@@ -67,7 +70,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
describe '#valid?' do
subject(:valid?) do
- project_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([project_file])
project_file.valid?
end
@@ -76,10 +79,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, file: '/file.yml' }
end
- let(:root_ref_sha) { project.repository.root_ref_sha }
-
- before do
- stub_project_blob(root_ref_sha, '/file.yml') { 'image: image:1.0' }
+ around do |example|
+ create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do
+ example.run
+ end
end
it { is_expected.to be_truthy }
@@ -99,10 +102,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, ref: 'master', file: '/file.yml' }
end
- let(:ref_sha) { project.commit('master').sha }
-
- before do
- stub_project_blob(ref_sha, '/file.yml') { 'image: image:1.0' }
+ around do |example|
+ create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do
+ example.run
+ end
end
it { is_expected.to be_truthy }
@@ -114,15 +117,16 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
end
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) }
- let(:root_ref_sha) { project.repository.root_ref_sha }
- before do
- stub_project_blob(root_ref_sha, '/secret_file.yml') { '' }
+ around do |example|
+ create_and_delete_files(project, { '/secret_file.yml' => '' }) do
+ example.run
+ end
end
it 'returns false' do
expect(valid?).to be_falsy
- expect(project_file.error_message).to include("Project `#{project.full_path}` file `/xxxxxxxxxxx.yml` is empty!")
+ expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxx.yml` is empty!")
end
end
@@ -146,7 +150,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
it 'returns false' do
expect(valid?).to be_falsy
- expect(project_file.error_message).to include("Project `#{project.full_path}` file `/xxxxxxxxxxxxxxxxxxx.yml` does not exist!")
+ expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxxxxxxxxxx.yml` does not exist!")
end
end
@@ -157,7 +161,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
it 'returns false' do
expect(valid?).to be_falsy
- expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!')
+ expect(project_file.error_message).to include('Included file `invalid-file` does not have YAML extension!')
end
end
@@ -200,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
is_expected.to include(
user: user,
project: project,
- sha: project.commit('master').id,
+ sha: project_sha,
parent_pipeline: parent_pipeline,
variables: project.predefined_variables.to_runner_variables)
end
@@ -216,45 +220,43 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do
it {
is_expected.to eq(
context_project: context_project.full_path,
- context_sha: '12345',
+ context_sha: project_sha,
type: :file,
- location: '/file.yml',
- blob: "http://localhost/#{project.full_path}/-/blob/#{project.commit('master').id}/file.yml",
- raw: "http://localhost/#{project.full_path}/-/raw/#{project.commit('master').id}/file.yml",
+ location: 'file.yml',
+ blob: "http://localhost/#{project.full_path}/-/blob/#{project_sha}/file.yml",
+ raw: "http://localhost/#{project.full_path}/-/raw/#{project_sha}/file.yml",
extra: { project: project.full_path, ref: 'HEAD' }
)
}
context 'when project name and ref include masked variables' do
+ let(:project_name) { 'my_project_name' }
+ let(:branch_name) { 'merge-commit-analyze-after' }
+ let(:project) { create(:project, :repository, name: project_name) }
+ let(:namespace_path) { project.namespace.full_path }
+ let(:included_project_sha) { project.commit(branch_name).sha }
+
let(:variables) do
Gitlab::Ci::Variables::Collection.new(
[
- { key: 'VAR1', value: 'a_secret_variable_value1', masked: true },
- { key: 'VAR2', value: 'a_secret_variable_value2', masked: true }
+ { key: 'VAR1', value: project_name, masked: true },
+ { key: 'VAR2', value: branch_name, masked: true }
])
end
- let(:params) { { project: 'a_secret_variable_value1', ref: 'a_secret_variable_value2', file: '/file.yml' } }
+ let(:params) { { project: project.full_path, ref: branch_name, file: '/file.yml' } }
it {
is_expected.to eq(
context_project: context_project.full_path,
- context_sha: '12345',
+ context_sha: project_sha,
type: :file,
- location: '/file.yml',
- blob: nil,
- raw: nil,
- extra: { project: 'xxxxxxxxxxxxxxxxxxxxxxxx', ref: 'xxxxxxxxxxxxxxxxxxxxxxxx' }
+ location: 'file.yml',
+ blob: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/blob/#{included_project_sha}/file.yml",
+ raw: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/raw/#{included_project_sha}/file.yml",
+ extra: { project: "#{namespace_path}/xxxxxxxxxxxxxxx", ref: 'xxxxxxxxxxxxxxxxxxxxxxxxxx' }
)
}
end
end
-
- private
-
- def stub_project_blob(ref, path)
- allow_next_instance_of(Repository) do |instance|
- allow(instance).to receive(:blob_data_at).with(ref, path) { yield }
- end
- end
end
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 8d93cdcf378..2ce3c257a43 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Remote do
+RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_authoring do
include StubRequests
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) }
@@ -55,7 +55,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do
describe "#valid?" do
subject(:valid?) do
- remote_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([remote_file])
remote_file.valid?
end
@@ -138,7 +138,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do
describe "#error_message" do
subject(:error_message) do
- remote_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([remote_file])
remote_file.error_message
end
diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
index 074e7a1d32d..83e98874118 100644
--- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::File::Template do
+RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_authoring do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -46,7 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Template do
describe "#valid?" do
subject(:valid?) do
- template_file.validate!
+ Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([template_file])
template_file.valid?
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
index 5f321a696c9..11c79e19cff 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb
@@ -17,11 +17,14 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
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' }]
+ [
+ { local: 'file.yml' },
+ { file: 'file.yml', project: 'namespace/project' },
+ { component: 'gitlab.com/org/component@1.0' },
+ { remote: 'https://example.com/.gitlab-ci.yml' },
+ { template: 'file.yml' },
+ { artifact: 'generated.yml', job: 'test' }
+ ]
end
subject(:process) { matcher.process(locations) }
@@ -30,6 +33,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
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::Component),
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)
@@ -42,8 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
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`'
+ /`{"invalid":"file.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
@@ -53,8 +56,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
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`'
+ /`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
end
@@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
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`"
+ /Each include must use only one of:/
)
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
index 7c7252c6b0e..a219666f24e 100644
--- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb
@@ -25,6 +25,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
my_test:
script: echo Hello World
YAML
+ 'myfolder/file3.yml' => <<~YAML,
+ my_deploy:
+ script: echo Hello World
+ YAML
'nested_configs.yml' => <<~YAML
include:
- local: myfolder/file1.yml
@@ -58,16 +62,63 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
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)
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file3.yml' }, context)
]
end
it 'returns an array of file objects' do
- expect(process.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml')
+ expect(process.map(&:location)).to contain_exactly(
+ 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml'
+ )
+ end
+
+ it 'adds files to the expandset' do
+ expect { process }.to change { context.expandset.count }.by(3)
+ end
+
+ it 'calls Gitaly only once for all files', :request_store do
+ # 1 for project.commit.id, 1 for the files
+ expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(2)
+ end
+ end
+
+ context 'when files are project files' do
+ let_it_be(:included_project) { create(:project, :repository, namespace: project.namespace, creator: user) }
+
+ let(:files) do
+ [
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file1.yml', project: included_project.full_path }, context
+ ),
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file2.yml', project: included_project.full_path }, context
+ ),
+ Gitlab::Ci::Config::External::File::Project.new(
+ { file: 'myfolder/file3.yml', project: included_project.full_path }, context
+ )
+ ]
+ end
+
+ around(:all) do |example|
+ create_and_delete_files(included_project, project_files) do
+ example.run
+ end
+ end
+
+ it 'returns an array of file objects' do
+ expect(process.map(&:location)).to contain_exactly(
+ 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml'
+ )
end
it 'adds files to the expandset' do
- expect { process }.to change { context.expandset.count }.by(2)
+ expect { process }.to change { context.expandset.count }.by(3)
+ end
+
+ it 'calls Gitaly only once for all files', :request_store do
+ # 1 for project.commit.id, 3 for the sha check, 1 for the files
+ expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(5)
end
end
@@ -99,7 +150,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
end
end
- context 'when max_includes is exceeded' do
+ context 'when total file count exceeds max_includes' do
context 'when files are nested' do
let(:files) do
[
@@ -107,11 +158,8 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
]
end
- before do
- allow(context).to receive(:max_includes).and_return(1)
- end
-
it 'raises Processor::IncludeError' do
+ allow(context).to receive(:max_includes).and_return(1)
expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError)
end
end
@@ -124,13 +172,36 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
]
end
- before do
+ it 'raises Mapper::TooManyIncludesError' do
allow(context).to receive(:max_includes).and_return(1)
+ expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
end
+ end
- it 'raises Mapper::TooManyIncludesError' do
+ context 'when files are duplicates' 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/file1.yml' }, context),
+ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context)
+ ]
+ end
+
+ it 'raises error' do
+ allow(context).to receive(:max_includes).and_return(2)
expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError)
end
+
+ context 'when FF ci_includes_count_duplicates is disabled' do
+ before do
+ stub_feature_flags(ci_includes_count_duplicates: false)
+ end
+
+ it 'does not raise error' do
+ allow(context).to receive(:max_includes).and_return(2)
+ expect { process }.not_to raise_error
+ 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 9d0e57d4292..b3115617084 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -2,9 +2,7 @@
require 'spec_helper'
-# This will be use with the FF ci_refactoring_external_mapper_verifier in the next MR.
-# It can be removed when the FF is removed.
-RSpec.shared_context 'gitlab_ci_config_external_mapper' do
+RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do
include StubRequests
include RepoHelpers
@@ -124,7 +122,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do
end
it 'returns ambigious specification error' do
- expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, '`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`')
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are:/)
end
end
@@ -138,7 +136,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do
end
it 'returns ambigious specification error' do
- expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`')
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /Each include must use only one of/)
end
end
@@ -168,7 +166,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do
an_instance_of(Gitlab::Ci::Config::External::File::Project))
end
- it_behaves_like 'logging config file fetch', 'config_file_fetch_project_content_duration_s', 2
+ it_behaves_like 'logging config file fetch', 'config_file_fetch_project_content_duration_s', 1
end
end
@@ -232,9 +230,20 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do
expect { process }.not_to raise_error
end
- it 'has expanset with one' do
+ it 'has expanset with two' do
process
- expect(context.expandset.size).to eq(1)
+ expect(context.expandset.size).to eq(2)
+ end
+
+ context 'when FF ci_includes_count_duplicates is disabled' do
+ before do
+ stub_feature_flags(ci_includes_count_duplicates: false)
+ end
+
+ it 'has expanset with one' do
+ process
+ expect(context.expandset.size).to eq(1)
+ end
end
end
@@ -464,7 +473,3 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do
end
end
end
-
-RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do
- it_behaves_like 'gitlab_ci_config_external_mapper'
-end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index c9efaf2e1af..bb65c2ef10c 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
- "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
+ "Local file `lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
)
end
end
@@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
- "Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
+ "Included file `lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
)
end
end
@@ -313,7 +313,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
expect(context.includes).to contain_exactly(
{ type: :local,
- location: '/local/file.yml',
+ location: '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: {},
@@ -334,14 +334,14 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
context_project: project.full_path,
context_sha: sha },
{ type: :file,
- location: '/templates/my-workflow.yml',
+ 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: sha },
{ type: :local,
- location: '/templates/my-build.yml',
+ location: 'templates/my-build.yml',
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml",
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml",
extra: {},
@@ -400,6 +400,44 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
end
end
+ describe 'include:component' do
+ let(:values) do
+ {
+ include: { component: "#{Gitlab.config.gitlab.host}/#{another_project.full_path}/component-x@master" },
+ image: 'image:1.0'
+ }
+ end
+
+ let(:other_project_files) do
+ {
+ '/component-x/template.yml' => <<~YAML
+ component_x_job:
+ script: echo Component X
+ YAML
+ }
+ end
+
+ before do
+ another_project.add_developer(user)
+ end
+
+ it 'appends the file to the values' do
+ output = processor.perform
+ expect(output.keys).to match_array([:image, :component_x_job])
+ end
+
+ context 'when feature flag ci_include_components is disabled' do
+ before do
+ stub_feature_flags(ci_include_components: false)
+ end
+
+ it 'returns an error' do
+ expect { processor.perform }
+ .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./)
+ end
+ end
+ end
+
context 'when a valid project file is defined' do
let(:values) do
{
@@ -465,7 +503,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
expect(context.includes).to contain_exactly(
{ type: :file,
- location: '/templates/my-build.yml',
+ location: 'templates/my-build.yml',
blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml",
raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml",
extra: { project: another_project.full_path, ref: 'HEAD' },
@@ -474,7 +512,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel
{ 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',
+ location: 'templates/my-test.yml',
extra: { project: another_project.full_path, ref: 'HEAD' },
context_project: project.full_path,
context_sha: sha }
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index e2bb55f3854..227b62d8ce8 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Config::External::Rules do
+RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_authoring do
let(:rule_hashes) {}
subject(:rules) { described_class.new(rule_hashes) }
diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb
new file mode 100644
index 00000000000..4b34553f55e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/yaml_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do
+ describe '.load!' do
+ it 'loads a single-doc YAML file' do
+ yaml = <<~YAML
+ image: 'image:1.0'
+ texts:
+ nested_key: 'value1'
+ more_text:
+ more_nested_key: 'value2'
+ YAML
+
+ config = described_class.load!(yaml)
+
+ expect(config).to eq({
+ image: 'image:1.0',
+ texts: {
+ nested_key: 'value1',
+ more_text: {
+ more_nested_key: 'value2'
+ }
+ }
+ })
+ end
+
+ it 'loads the first document from a multi-doc YAML file' do
+ yaml = <<~YAML
+ spec:
+ inputs:
+ test_input:
+ ---
+ image: 'image:1.0'
+ texts:
+ nested_key: 'value1'
+ more_text:
+ more_nested_key: 'value2'
+ YAML
+
+ config = described_class.load!(yaml)
+
+ expect(config).to eq({
+ spec: {
+ inputs: {
+ test_input: nil
+ }
+ }
+ })
+ end
+
+ context 'when ci_multi_doc_yaml is disabled' do
+ before do
+ stub_feature_flags(ci_multi_doc_yaml: false)
+ end
+
+ it 'loads a single-doc YAML file' do
+ yaml = <<~YAML
+ image: 'image:1.0'
+ texts:
+ nested_key: 'value1'
+ more_text:
+ more_nested_key: 'value2'
+ YAML
+
+ config = described_class.load!(yaml)
+
+ expect(config).to eq({
+ image: 'image:1.0',
+ texts: {
+ nested_key: 'value1',
+ more_text: {
+ more_nested_key: 'value2'
+ }
+ }
+ })
+ end
+
+ it 'loads the first document from a multi-doc YAML file' do
+ yaml = <<~YAML
+ spec:
+ inputs:
+ test_input:
+ ---
+ image: 'image:1.0'
+ texts:
+ nested_key: 'value1'
+ more_text:
+ more_nested_key: 'value2'
+ YAML
+
+ config = described_class.load!(yaml)
+
+ expect(config).to eq({
+ spec: {
+ inputs: {
+ test_input: nil
+ }
+ }
+ })
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 4b750cf3bcf..2c07e4d2224 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
- context 'when slash used' do
+ context 'when */ used' do
let(:cron) { '*/10 */6 */10 */10 *' }
let(:cron_timezone) { 'UTC' }
@@ -63,7 +63,7 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
- context 'when range and slash used' do
+ context 'when range and / are used' do
let(:cron) { '3-59/10 * * * *' }
let(:cron_timezone) { 'UTC' }
@@ -74,6 +74,17 @@ RSpec.describe Gitlab::Ci::CronParser do
end
end
+ context 'when / is used' do
+ let(:cron) { '3/10 * * * *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it_behaves_like returns_time_for_epoch
+
+ it 'returns specific time' do
+ expect(subject.min).to be_in([3, 13, 23, 33, 43, 53])
+ end
+ end
+
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/interpolation/access_spec.rb
new file mode 100644
index 00000000000..9f6108a328d
--- /dev/null
+++ b/spec/lib/gitlab/ci/interpolation/access_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_authoring do
+ subject { described_class.new(access, ctx) }
+
+ let(:access) do
+ 'inputs.data'
+ end
+
+ let(:ctx) do
+ { inputs: { data: 'abcd' }, env: { 'ENV' => 'dev' } }
+ end
+
+ it 'properly evaluates the access pattern' do
+ expect(subject.value).to eq 'abcd'
+ end
+
+ context 'when there are too many objects in the access path' do
+ let(:access) { 'a.b.c.d.e.f.g.h' }
+
+ it 'only support MAX_ACCESS_OBJECTS steps' do
+ expect(subject.objects.count).to eq 5
+ end
+ end
+
+ context 'when access expression size is too large' do
+ before do
+ stub_const("#{described_class}::MAX_ACCESS_BYTESIZE", 10)
+ end
+
+ it 'returns an error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.first)
+ .to eq 'maximum interpolation expression size exceeded'
+ end
+ end
+
+ context 'when there are not enough objects in the access path' do
+ let(:access) { 'abc[123]' }
+
+ it 'returns an error when there are no objects found' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.first)
+ .to eq 'invalid interpolation access pattern'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb
new file mode 100644
index 00000000000..7f2be505d17
--- /dev/null
+++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_authoring do
+ subject { described_class.new(block, data, ctx) }
+
+ let(:data) do
+ 'inputs.data'
+ end
+
+ let(:block) do
+ "$[[ #{data} ]]"
+ end
+
+ let(:ctx) do
+ { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } }
+ end
+
+ it 'knows its content' do
+ expect(subject.content).to eq 'inputs.data'
+ end
+
+ it 'properly evaluates the access pattern' do
+ expect(subject.value).to eq 'abc'
+ end
+
+ describe '.match' do
+ it 'matches each block in a string' do
+ expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) }
+ .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2'])
+ end
+
+ it 'matches an empty block' do
+ expect { |b| described_class.match('$[[]]', &b) }
+ .to yield_with_args('$[[]]', '')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb
new file mode 100644
index 00000000000..e5987776e00
--- /dev/null
+++ b/spec/lib/gitlab/ci/interpolation/config_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_authoring do
+ subject { described_class.new(YAML.safe_load(config)) }
+
+ let(:config) do
+ <<~CFG
+ test:
+ spec:
+ env: $[[ inputs.env ]]
+
+ $[[ inputs.key ]]:
+ name: $[[ inputs.key ]]
+ script: my-value
+ CFG
+ end
+
+ describe '#replace!' do
+ it 'replaces each od the nodes with a block return value' do
+ result = subject.replace! { |node| "abc#{node}cde" }
+
+ expect(result).to eq({
+ 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } },
+ 'abc$[[ inputs.key ]]cde' => {
+ 'abcnamecde' => 'abc$[[ inputs.key ]]cde',
+ 'abcscriptcde' => 'abcmy-valuecde'
+ }
+ })
+ end
+ end
+
+ context 'when config size is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_NODES", 7)
+ end
+
+ it 'returns a config size error' do
+ replaced = 0
+
+ subject.replace! { replaced += 1 }
+
+ expect(replaced).to eq 4
+ expect(subject.errors.size).to eq 1
+ expect(subject.errors.first).to eq 'config too large'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/interpolation/context_spec.rb
new file mode 100644
index 00000000000..ada896f4980
--- /dev/null
+++ b/spec/lib/gitlab/ci/interpolation/context_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_authoring do
+ subject { described_class.new(ctx) }
+
+ let(:ctx) do
+ { inputs: { key: 'abc' } }
+ end
+
+ describe '#depth' do
+ it 'returns a max depth of the hash' do
+ expect(subject.depth).to eq 2
+ end
+ end
+
+ context 'when interpolation context is too complex' do
+ let(:ctx) do
+ { inputs: { key: { aaa: { bbb: 'ccc' } } } }
+ end
+
+ it 'raises an exception' do
+ expect { described_class.new(ctx) }
+ .to raise_error(described_class::ContextTooComplexError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/interpolation/template_spec.rb
new file mode 100644
index 00000000000..8a243b4db05
--- /dev/null
+++ b/spec/lib/gitlab/ci/interpolation/template_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_authoring do
+ subject { described_class.new(YAML.safe_load(config), ctx) }
+
+ let(:config) do
+ <<~CFG
+ test:
+ spec:
+ env: $[[ inputs.env ]]
+
+ $[[ inputs.key ]]:
+ name: $[[ inputs.key ]]
+ script: my-value
+ CFG
+ end
+
+ let(:ctx) do
+ { inputs: { env: 'dev', key: 'abc' } }
+ end
+
+ it 'collects interpolation blocks' do
+ expect(subject.size).to eq 2
+ end
+
+ it 'interpolates the values properly' do
+ expect(subject.interpolated).to eq YAML.safe_load <<~RESULT
+ test:
+ spec:
+ env: dev
+
+ abc:
+ name: abc
+ script: my-value
+ RESULT
+ end
+
+ context 'when interpolation can not be performed' do
+ let(:config) { '$[[ xxx.yyy ]]: abc' }
+
+ it 'does not interpolate the config' do
+ expect(subject).not_to be_valid
+ expect(subject.interpolated).to be_nil
+ end
+ end
+
+ context 'when template consists of nested arrays with hashes and values' do
+ let(:config) do
+ <<~CFG
+ test:
+ - a-$[[ inputs.key ]]-b
+ - c-$[[ inputs.key ]]-d:
+ d-$[[ inputs.key ]]-e
+ val: 1
+ CFG
+ end
+
+ it 'performs a valid interpolation' do
+ result = { 'test' => ['a-abc-b', { 'c-abc-d' => 'd-abc-e', 'val' => 1 }] }
+
+ expect(subject).to be_valid
+ expect(subject.interpolated).to eq result
+ end
+ end
+
+ context 'when template contains symbols that need interpolation' do
+ subject do
+ described_class.new({ '$[[ inputs.key ]]'.to_sym => 'cde' }, ctx)
+ end
+
+ it 'performs a valid interpolation' do
+ expect(subject).to be_valid
+ expect(subject.interpolated).to eq({ 'abc' => 'cde' })
+ end
+ end
+
+ context 'when template is too large' do
+ before do
+ stub_const('Gitlab::Ci::Interpolation::Config::MAX_NODES', 1)
+ end
+
+ it 'returns an error' do
+ expect(subject.interpolated).to be_nil
+ expect(subject.errors.count).to eq 1
+ expect(subject.errors.first).to eq 'config too large'
+ end
+ end
+
+ context 'when there are too many interpolation blocks' do
+ before do
+ stub_const("#{described_class}::MAX_BLOCKS", 1)
+ end
+
+ it 'returns an error' do
+ expect(subject.interpolated).to be_nil
+ expect(subject.errors.count).to eq 1
+ expect(subject.errors.first).to eq 'too many interpolation blocks'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
index 30bcce21be2..6772c62ab93 100644
--- a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe Gitlab::Ci::Parsers::Instrumentation do
Class.new do
prepend Gitlab::Ci::Parsers::Instrumentation
- def parse!(arg1, arg2)
+ def parse!(arg1, arg2:)
"parse #{arg1} #{arg2}"
end
end
end
it 'sets metrics for duration of parsing' do
- result = parser_class.new.parse!('hello', 'world')
+ result = parser_class.new.parse!('hello', arg2: 'world')
expect(result).to eq('parse hello world')
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 03cab021c17..5d2d22c04fc 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -203,24 +203,35 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
context 'and name is not provided' do
- context 'when CVE identifier exists' do
- it 'combines identifier with location to create name' do
+ context 'when location does not exist' do
+ let(:location) { nil }
+
+ it 'returns only identifier name' do
finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
- expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
+ expect(finding.name).to eq("CVE-2017-11429")
end
end
- context 'when CWE identifier exists' do
- it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
- expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
+ context 'when location exists' do
+ context 'when CVE identifier exists' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
+ expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
+ end
+ end
+
+ context 'when CWE identifier exists' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
+ expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
+ end
end
- end
- context 'when neither CVE nor CWE identifier exist' do
- it 'combines identifier with location to create name' do
- finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
- expect(finding.name).to eq("other-2017-11429 in yarn.lock")
+ context 'when neither CVE nor CWE identifier exist' do
+ it 'combines identifier with location to create name' do
+ finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
+ expect(finding.name).to eq("other-2017-11429 in yarn.lock")
+ end
end
end
end
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 16deeb6916f..31bffcbeb2a 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
@@ -2,208 +2,20 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do
+RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
-
- let(:prev_pipeline) { create(:ci_pipeline, project: project) }
- let(:new_commit) { create(:commit, project: project) }
- let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
-
- let(:command) do
- Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
- end
-
- let(:step) { described_class.new(pipeline, command) }
-
- before do
- create(:ci_build, :interruptible, :running, pipeline: prev_pipeline)
- create(:ci_build, :interruptible, :success, pipeline: prev_pipeline)
- create(:ci_build, :created, pipeline: prev_pipeline)
-
- create(:ci_build, :interruptible, pipeline: pipeline)
- end
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) }
+ let_it_be(:step) { described_class.new(pipeline, command) }
describe '#perform!' do
subject(:perform) { step.perform! }
- before do
- expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
- expect(build_statuses(pipeline)).to contain_exactly('pending')
- end
-
- context 'when auto-cancel is enabled' do
- before do
- project.update!(auto_cancel_pending_pipelines: 'enabled')
- end
-
- it 'cancels only previous interruptible builds' do
- perform
-
- expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled')
- expect(build_statuses(pipeline)).to contain_exactly('pending')
- end
-
- it 'logs canceled pipelines' do
- allow(Gitlab::AppLogger).to receive(:info)
-
- perform
-
- expect(Gitlab::AppLogger).to have_received(:info).with(
- class: described_class.name,
- message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}",
- canceled_pipeline_id: prev_pipeline.id,
- canceled_by_pipeline_id: pipeline.id,
- canceled_by_pipeline_source: pipeline.source
- )
- end
-
- it 'cancels the builds with 2 queries to avoid query timeout' do
- second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/
- recorder = ActiveRecord::QueryRecorder.new { perform }
- second_query = recorder.occurrences.keys.filter { |occ| occ =~ second_query_regex }
-
- expect(second_query).to be_one
- end
-
- context 'when the previous pipeline has a child pipeline' do
- let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) }
-
- context 'when the child pipeline has interruptible running jobs' do
- before do
- create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
- create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
- end
-
- it 'cancels all child pipeline builds' do
- expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running')
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
- end
-
- context 'when the child pipeline includes completed interruptible jobs' do
- before do
- create(:ci_build, :interruptible, :failed, pipeline: child_pipeline)
- create(:ci_build, :interruptible, :success, pipeline: child_pipeline)
- end
-
- it 'cancels all child pipeline builds with a cancelable_status' do
- expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success')
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success')
- end
- end
- end
-
- context 'when the child pipeline has started non-interruptible job' do
- before do
- create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
- # non-interruptible started
- create(:ci_build, :success, pipeline: child_pipeline)
- end
+ it 'enqueues CancelRedundantPipelinesWorker' do
+ expect(Ci::CancelRedundantPipelinesWorker).to receive(:perform_async).with(pipeline.id)
- it 'does not cancel any child pipeline builds' do
- expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
- end
- end
-
- context 'when the child pipeline has non-interruptible non-started job' do
- before do
- create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
- end
-
- not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES
- context 'when the jobs are cancelable' do
- cancelable_not_started_statuses = Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES)
- cancelable_not_started_statuses.each do |status|
- it "cancels all child pipeline builds when build status #{status} included" do
- # non-interruptible but non-started
- create(:ci_build, status.to_sym, pipeline: child_pipeline)
-
- expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
- end
- end
- end
-
- context 'when the jobs are not cancelable' do
- not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES
- not_cancelable_not_started_statuses.each do |status|
- it "does not cancel child pipeline builds when build status #{status} included" do
- # non-interruptible but non-started
- create(:ci_build, status.to_sym, pipeline: child_pipeline)
-
- expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
-
- perform
-
- expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status)
- end
- end
- end
- end
- end
-
- context 'when the pipeline is a child pipeline' do
- let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
- let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) }
-
- before do
- create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
- create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
- end
-
- it 'does not cancel any builds' do
- expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
- expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
-
- perform
-
- expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
- expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
- end
- end
-
- context 'when the previous pipeline source is webide' do
- let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) }
-
- it 'does not cancel builds of the previous pipeline' do
- perform
-
- expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success')
- expect(build_statuses(pipeline)).to contain_exactly('pending')
- end
- end
+ subject
end
-
- context 'when auto-cancel is disabled' do
- before do
- project.update!(auto_cancel_pending_pipelines: 'disabled')
- end
-
- it 'does not cancel any build' do
- subject
-
- expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
- expect(build_statuses(pipeline)).to contain_exactly('pending')
- end
- end
- end
-
- private
-
- def build_statuses(pipeline)
- pipeline.builds.pluck(:status)
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
deleted file mode 100644
index bec80a43a76..00000000000
--- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments, feature_category: :continuous_integration do
- let_it_be(:project) { create(:project, :repository) }
- let_it_be(:user) { create(:user) }
-
- let(:stage) { build(:ci_stage, project: project, statuses: [job]) }
- let(:pipeline) { create(:ci_pipeline, project: project, stages: [stage]) }
-
- let(:command) do
- Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
- end
-
- let(:step) { described_class.new(pipeline, command) }
-
- describe '#perform!' do
- subject { step.perform! }
-
- before do
- stub_feature_flags(move_create_deployments_to_worker: false)
- job.pipeline = pipeline
- end
-
- context 'when a pipeline contains a deployment job' do
- let!(:job) { build(:ci_build, :start_review_app, project: project) }
- let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) }
-
- it 'creates a deployment record' do
- expect { subject }.to change { Deployment.count }.by(1)
-
- job.reset
- expect(job.deployment.project).to eq(job.project)
- expect(job.deployment.ref).to eq(job.ref)
- expect(job.deployment.sha).to eq(job.sha)
- expect(job.deployment.deployable).to eq(job)
- expect(job.deployment.deployable_type).to eq('CommitStatus')
- expect(job.deployment.environment).to eq(job.persisted_environment)
- end
-
- context 'when the corresponding environment does not exist' do
- let!(:environment) {}
-
- it 'does not create a deployment record' do
- expect { subject }.not_to change { Deployment.count }
-
- expect(job.deployment).to be_nil
- end
- end
- end
-
- context 'when a pipeline contains a teardown job' do
- let!(:job) { build(:ci_build, :stop_review_app, project: project) }
- let!(:environment) { create(:environment, name: job.expanded_environment_name) }
-
- it 'does not create a deployment record' do
- expect { subject }.not_to change { Deployment.count }
-
- expect(job.deployment).to be_nil
- end
- end
-
- context 'when a pipeline does not contain a deployment job' do
- let!(:job) { build(:ci_build, project: project) }
-
- it 'does not create any deployments' do
- expect { subject }.not_to change { Deployment.count }
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb
new file mode 100644
index 00000000000..b955d0e7cee
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Metrics, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'master', user: user, name: 'Build pipeline')
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ origin_ref: 'master')
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ subject(:run_chain) { step.perform! }
+
+ it 'does not break the chain' do
+ run_chain
+
+ expect(step.break?).to be false
+ end
+
+ context 'with pipeline name' do
+ it 'creates snowplow event' do
+ run_chain
+
+ expect_snowplow_event(
+ category: described_class.to_s,
+ action: 'create_pipeline_with_name',
+ project: pipeline.project,
+ user: pipeline.user,
+ namespace: pipeline.project.namespace
+ )
+ end
+ end
+
+ context 'without pipeline name' do
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'master', user: user)
+ end
+
+ it 'does not create snowplow event' do
+ run_chain
+
+ expect_no_snowplow_event
+ end
+ end
+end
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 9373888aada..df18e1e4f48 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :pipeline_execution do
+RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :continuous_integration do
let(:project) { create(:project, :test_repo) }
let_it_be(:user) { create(:user) }
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 47f172922a5..1a622000c1b 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
-require_dependency 're2'
+require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
+RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches, feature_category: :continuous_integration do
include StubFeatureFlags
let(:left) { double('left') }
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
index 9e7ea3e4ea4..a60b00457fb 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
-require_dependency 're2'
+require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
+RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches, feature_category: :continuous_integration do
include StubFeatureFlags
let(:left) { double('left') }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 1f7f800e238..3043d7f5381 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -12,953 +12,860 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au
let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage, when: 'on_success' } }
let(:previous_stages) { [] }
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], current_ci_stage) }
+ let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) }
- shared_examples 'build seed' do
- describe '#attributes' do
- subject { seed_build.attributes }
+ 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') }
+ it { is_expected.to include(when: 'on_failure') }
+ end
+
+ 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 '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') }
end
- context 'with job:when:delayed' do
- let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', options: { start_in: '3 hours' } } }
+ 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' }] } }
it { is_expected.to include(when: 'delayed', options: { 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' }] } }
+ 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' } }
- it { is_expected.to include(when: 'always') }
+ context 'with a manual job' do
+ context 'with a matched rule' do
+ let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) }
+
+ it { is_expected.to include(when: 'manual') }
end
context 'is not matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } }
+ let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) }
it { is_expected.to include(when: 'never') }
end
end
- context 'with job:rules:[when:delayed]' do
+ context 'with an automatic job' do
context 'is matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
+ let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) }
- it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) }
+ it { is_expected.to include(when: 'on_success') }
end
context 'is not matched' do
- let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } }
+ let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) }
it { is_expected.to include(when: 'never') }
end
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' }]) }
-
- it { is_expected.to include(when: 'manual') }
- end
-
- context 'is not matched' do
- let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) }
-
- it { is_expected.to include(when: 'never') }
- 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 an automatic job' do
- context 'is matched' do
- let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) }
+ 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
- it { is_expected.to include(when: 'on_success') }
- end
+ 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
- context 'is not matched' do
- let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) }
+ it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
+ it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
+ end
- it { is_expected.to include(when: 'never') }
- end
- end
+ context 'with cache:key' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: [{
+ key: 'a-value'
+ }]
+ }
end
- context 'with job:rules:[variables:]' do
+ it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) }
+
+ context 'with cache:key:files' do
let(:attributes) do
- { name: 'rspec',
+ {
+ 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' } }] }
+ cache: [{
+ key: {
+ files: ['VERSION']
+ }
+ }]
+ }
end
- 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
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')]
+ }
+ }
- 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' })
+ is_expected.to include(cache_options)
end
end
- context 'with job:tags' do
+ context 'with cache:key:prefix' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
- job_variables: [{ key: 'VARIABLE', value: 'value' }],
- tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE']
+ cache: [{
+ key: {
+ prefix: 'something'
+ }
+ }]
}
end
- it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) }
- it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) }
+ it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) }
end
- context 'with cache:key' do
+ context 'with cache:key:files and prefix' do
let(:attributes) do
{
name: 'rspec',
ref: 'master',
cache: [{
- key: 'a-value'
+ key: {
+ files: ['VERSION'],
+ prefix: 'something'
+ }
}]
}
end
- 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
-
- it 'includes cache options' do
- cache_options = {
- options: {
- cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')]
- }
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')]
}
+ }
- is_expected.to include(cache_options)
- end
- end
-
- context 'with cache:key:prefix' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- cache: [{
- key: {
- prefix: 'something'
- }
- }]
- }
- end
-
- it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) }
+ is_expected.to include(cache_options)
end
+ end
+ end
- context 'with cache:key:files and prefix' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- cache: [{
- key: {
- files: ['VERSION'],
- prefix: 'something'
- }
- }]
- }
- end
+ context 'with empty cache' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {}
+ }
+ end
- it 'includes cache options' do
- cache_options = {
- options: {
- cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')]
- }
- }
+ it { is_expected.to include({}) }
+ end
- is_expected.to include(cache_options)
- end
- end
+ context 'with allow_failure' do
+ let(:options) do
+ { allow_failure_criteria: { exit_codes: [42] } }
end
- context 'with empty cache' do
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- cache: {}
- }
- end
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always' }]
+ end
- it { is_expected.to include({}) }
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ options: options,
+ rules: rules
+ }
end
- context 'with allow_failure' do
- let(:options) do
- { allow_failure_criteria: { exit_codes: [42] } }
- end
+ context 'when rules does not override allow_failure' do
+ it { is_expected.to match a_hash_including(options: options) }
+ end
+ context 'when rules set allow_failure to true' do
let(:rules) do
- [{ if: '$VAR == null', when: 'always' }]
+ [{ if: '$VAR == null', when: 'always', allow_failure: true }]
end
- let(:attributes) do
- {
- name: 'rspec',
- ref: 'master',
- options: options,
- rules: rules
- }
- end
-
- context 'when rules does not override allow_failure' do
- it { is_expected.to match a_hash_including(options: options) }
- end
-
- context 'when rules set allow_failure to true' do
- let(:rules) do
- [{ if: '$VAR == null', when: 'always', allow_failure: true }]
- end
-
- it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
-
- context 'when options contain other static values' do
- let(:options) do
- { image: 'busybox', allow_failure_criteria: { exit_codes: [42] } }
- end
-
- it { is_expected.to match a_hash_including(options: { image: 'busybox', allow_failure_criteria: nil }) }
+ it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
+ end
- 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
+ context 'when rules set allow_failure to false' do
+ let(:rules) do
+ [{ if: '$VAR == null', when: 'always', allow_failure: false }]
end
- context 'when rules set allow_failure to false' do
- let(:rules) do
- [{ if: '$VAR == null', when: 'always', allow_failure: false }]
- end
-
- it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) }
- 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' },
+ 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' }],
- 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
-
- context 'when root_variables_inheritance is true' do
- let(:root_variables_inheritance) { true }
+ job_variables: [{ key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }],
+ root_variables_inheritance: root_variables_inheritance }
+ 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' }]
- )
- end
- 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
- context 'when root_variables_inheritance is false' do
- let(:root_variables_inheritance) { false }
+ context 'when root_variables_inheritance is true' do
+ let(:root_variables_inheritance) { true }
- it 'returns job variables' do
- expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }]
- )
- 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' }]
+ )
end
+ end
- context 'when root_variables_inheritance is an array' do
- let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
+ context 'when root_variables_inheritance is false' do
+ let(:root_variables_inheritance) { false }
- 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
+ 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 the pipeline has not a variable' do
- let(:root_variables_inheritance) { true }
+ context 'when root_variables_inheritance is an array' do
+ let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) }
- it 'returns seed yaml variables' do
+ it 'returns calculated yaml variables' do
expect(subject[:yaml_variables]).to match_array(
- [{ key: 'VAR2', value: 'var 2' },
- { key: 'VAR3', value: 'var 3' }])
+ [{ key: 'VAR1', value: 'var overridden pipeline 1' },
+ { key: 'VAR2', value: 'var 2' },
+ { key: 'VAR3', value: 'var 3' }]
+ )
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 }
+ 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
- let(:root_variables_inheritance) { true }
+ 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 'when the rules use job variables' do
- let(:rules) do
- [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }]
- end
+ let(:root_variables_inheritance) { true }
- 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
+ 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
- context 'when the rules use root variables' do
- let(:root_variables) do
- [{ key: 'VAR2', value: 'var pipeline 2' }]
- 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' })
+ end
+ end
- let(:rules) do
- [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }]
- end
+ context 'when the rules use root variables' do
+ let(:root_variables) do
+ [{ key: 'VAR2', value: 'var pipeline 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
+ let(:rules) do
+ [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }]
+ end
- context 'when the root_variables_inheritance is false' do
- let(:root_variables_inheritance) { false }
+ 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
- it 'does not recalculate the variables' do
- expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
- end
+ context 'when the root_variables_inheritance is false' do
+ let(:root_variables_inheritance) { false }
+
+ it 'does not recalculate the variables' do
+ expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' })
end
end
end
end
+ end
+
+ describe '#bridge?' do
+ subject { seed_build.bridge? }
+
+ context 'when job is a downstream bridge' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
+ end
- describe '#bridge?' do
- subject { seed_build.bridge? }
+ it { is_expected.to be_truthy }
- context 'when job is a downstream bridge' do
+ context 'when trigger definition is empty' do
let(:attributes) do
- { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
+ { name: 'rspec', ref: 'master', options: { trigger: '' } }
end
- it { is_expected.to be_truthy }
-
- context 'when trigger definition is empty' do
- let(:attributes) do
- { name: 'rspec', ref: 'master', options: { trigger: '' } }
- end
+ it { is_expected.to be_falsey }
+ end
+ end
- it { is_expected.to be_falsey }
- end
+ context 'when job is an upstream bridge' do
+ let(:attributes) do
+ { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } }
end
- context 'when job is an upstream bridge' do
+ it { is_expected.to be_truthy }
+
+ context 'when upstream definition is empty' do
let(:attributes) do
- { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } }
+ { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } }
end
- it { is_expected.to be_truthy }
+ 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
+ context 'when job is not a bridge' do
+ it { is_expected.to be_falsey }
+ end
+ end
- it { is_expected.to be_falsey }
- end
- end
+ describe '#to_resource' do
+ subject { seed_build.to_resource }
- context 'when job is not a bridge' do
- it { is_expected.to be_falsey }
- end
+ it 'memoizes a resource object' do
+ expect(subject.object_id).to eq seed_build.to_resource.object_id
end
- describe '#to_resource' do
- subject { seed_build.to_resource }
+ it 'can not be persisted without explicit assignment' do
+ pipeline.save!
- it 'memoizes a resource object' do
- expect(subject.object_id).to eq seed_build.to_resource.object_id
- end
+ expect(subject).not_to be_persisted
+ end
+ end
- it 'can not be persisted without explicit assignment' do
- pipeline.save!
+ describe 'applying job inclusion policies' do
+ subject { seed_build }
- expect(subject).not_to be_persisted
+ context 'when no branch policy is specified' do
+ let(:attributes) do
+ { name: 'rspec' }
end
- end
- describe 'applying job inclusion policies' do
- subject { seed_build }
+ it { is_expected.to be_included }
+ end
- context 'when no branch policy is specified' do
+ context 'when branch policy does not match' do
+ context 'when using only' do
let(:attributes) do
- { name: 'rspec' }
+ { name: 'rspec', only: { refs: ['deploy'] } }
end
- it { is_expected.to be_included }
+ it { is_expected.not_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 }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ['deploy'] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: ['deploy'] } }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.to be_included }
+ context 'with both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[deploy] },
+ except: { refs: %w[deploy] }
+ }
end
- context 'with both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[deploy] },
- except: { refs: %w[deploy] }
- }
- end
-
- it { is_expected.not_to be_included }
- end
+ it { is_expected.not_to be_included }
end
+ end
- 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 }
+ context 'when branch regexp policy does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[/^deploy$/] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[/^deploy$/] } }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[/^deploy$/] } }
end
- context 'with both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[/^deploy$/] },
- except: { refs: %w[/^deploy$/] }
- }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'with both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[/^deploy$/] },
+ except: { refs: %w[/^deploy$/] }
+ }
end
- end
- 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.not_to be_included }
+ end
+ end
- it { is_expected.to be_included }
+ context 'when branch policy matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[deploy master] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[deploy master] } }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[deploy master] } }
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
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.not_to be_included }
+ 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
- end
- context 'when keyword policy matches' do
- context 'when using only' do
- let(:attributes) do
- { name: 'rspec', only: { refs: %w[branches] } }
- end
+ it { is_expected.not_to be_included }
+ end
+ end
- it { is_expected.to be_included }
+ context 'when keyword policy matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[branches] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[branches] } }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[branches] } }
end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[branches] },
- except: { refs: %w[branches] }
- }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[branches] },
+ except: { refs: %w[branches] }
+ }
end
- end
- 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
+ end
- it { is_expected.not_to be_included }
+ context 'when keyword policy does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: %w[tags] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[tags] } }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[tags] } }
end
- context 'when using both only and except policies' do
- let(:attributes) do
- {
- name: 'rspec',
- only: { refs: %w[tags] },
- except: { refs: %w[tags] }
- }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: { refs: %w[tags] },
+ except: { refs: %w[tags] }
+ }
end
- end
- context 'with source-keyword policy' do
- using RSpec::Parameterized
+ it { is_expected.not_to be_included }
+ end
+ end
- let(:pipeline) do
- build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project)
- end
+ context 'with source-keyword policy' do
+ using RSpec::Parameterized
- 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
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project)
+ end
- with_them do
- context 'using an only policy' do
- let(:attributes) do
- { name: 'rspec', only: { refs: [keyword] } }
- 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
- it { is_expected.to be_included }
+ with_them do
+ context 'using an only policy' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: [keyword] } }
end
- context 'using an except policy' do
- let(:attributes) do
- { name: 'rspec', except: { refs: [keyword] } }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ 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] }
- }
- end
+ 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
- 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] }
+ it { is_expected.not_to be_included }
end
+ end
+ end
- with_them do
- context 'using an only policy' do
- let(:attributes) do
- { name: 'rspec', only: { refs: [keyword] } }
- 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
- it { is_expected.not_to be_included }
+ with_them do
+ context 'using an only policy' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: [keyword] } }
end
- context 'using an except policy' do
- let(:attributes) do
- { name: 'rspec', except: { refs: [keyword] } }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.to be_included }
+ 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] }
- }
- end
+ it { is_expected.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 '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 }
+ context 'when repository path matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
end
- context 'when using except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
- end
+ it { is_expected.to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', 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}"] }
- }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.not_to be_included }
+ 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}"]
- }
- }
- end
-
- it { is_expected.not_to be_included }
- end
+ it { is_expected.not_to be_included }
end
- 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 }
+ 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 except' do
- let(:attributes) do
- { name: 'rspec', except: { refs: %w[branches@fork] } }
- end
+ it { is_expected.not_to be_included }
+ end
+ end
- it { is_expected.to be_included }
+ context 'when repository path does not match' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { 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] }
- }
- end
+ it { is_expected.not_to be_included }
+ end
- it { is_expected.not_to be_included }
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: %w[branches@fork] } }
end
+
+ it { is_expected.to be_included }
end
- context 'using rules:' do
- using RSpec::Parameterized
+ 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
- let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } }
+ it { is_expected.not_to be_included }
+ end
+ 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
+ context 'using rules:' do
+ using RSpec::Parameterized
- with_them do
- it { is_expected.not_to be_included }
+ let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } }
- it 'still correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
- end
- 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
- 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 }
+ with_them do
+ it { is_expected.not_to be_included }
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'always')
- end
+ it 'still correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
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' }]]
- ]
- end
-
- 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
+ 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
- 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 }
+ 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
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
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: 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
- 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')
- end
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_failure')
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])
- 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' }]]
+ ]
end
- 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
-
- with_them do
- it { is_expected.not_to be_included }
+ with_them do
+ it { is_expected.to be_included }
- it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
- end
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' })
end
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' }]]
- ]
- 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
- 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: 'always')
- end
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
end
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 **/*] } }]]
- ]
- 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
+ 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
- context 'with no matching 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' }]],
- [[{ 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' }]]
+ [[{ 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
@@ -971,249 +878,291 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au
end
end
- context 'with a rule using CI_ENVIRONMENT_NAME variable' do
- let(:rule_set) do
- [{ if: '$CI_ENVIRONMENT_NAME == "test"' }]
+ 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
- context 'when environment:name satisfies the rule' do
- let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success' } }
-
+ with_them do
it { is_expected.to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'on_success')
+ expect(seed_build.attributes).to include(when: 'always')
end
end
+ end
- 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
+ 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
- context 'when environment:name is not set' do
- it { is_expected.not_to be_included }
+ with_them do
+ it { is_expected.to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'never')
+ expect(seed_build.attributes).to include(when: 'on_success')
end
end
end
+ end
- context 'with no rules' do
- let(:rule_set) { [] }
+ 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' }]]
+ ]
+ end
+ 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
+
+ context 'with a rule using CI_ENVIRONMENT_NAME variable' do
+ let(:rule_set) do
+ [{ if: '$CI_ENVIRONMENT_NAME == "test"' }]
+ end
- context 'with invalid rules raising error' do
- let(:rule_set) do
- [
- { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' }
- ]
+ 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: 'on_success')
end
+ end
+
+ 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
- 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 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: 'never')
end
end
end
- end
-
- describe 'applying needs: dependency' do
- subject { seed_build }
- let(:needs_count) { 1 }
+ context 'with no rules' do
+ let(:rule_set) { [] }
- let(:needs_attributes) do
- Array.new(needs_count, name: 'build')
- end
+ it { is_expected.not_to be_included }
- let(:attributes) do
- {
- name: 'rspec',
- needs_attributes: needs_attributes
- }
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
end
- context 'when build job is not present in prior stages' do
- it "is included" do
- is_expected.to be_included
+ context 'with invalid rules raising error' do
+ let(:rule_set) do
+ [
+ { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' }
+ ]
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
+ it { is_expected.not_to be_included }
- context 'when the needed job is optional' do
- let(:needs_attributes) { [{ name: 'build', optional: true }] }
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
- it "does not return an error" do
- expect(subject.errors).to be_empty
- 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 'when build job is part of prior stages' do
- let(:stage_attributes) do
- {
- name: 'build',
- index: 0,
- builds: [{ name: 'build' }]
- }
- end
+ describe 'applying needs: dependency' do
+ subject { seed_build }
- let(:stage_seed) do
- Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, [])
- end
+ let(:needs_count) { 1 }
- let(:previous_stages) { [stage_seed] }
+ let(:needs_attributes) do
+ Array.new(needs_count, name: 'build')
+ end
- it "is included" do
- is_expected.to be_included
- end
+ let(:attributes) do
+ {
+ name: 'rspec',
+ needs_attributes: needs_attributes
+ }
+ end
- it "does not have errors" do
- expect(subject.errors).to be_empty
- end
+ context 'when build job is not present in prior stages' do
+ it "is included" do
+ is_expected.to be_included
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' job needs 'build' job, but 'build' is not in any previous stage")
+ end
- it 'is included' do
- is_expected.to be_included
- end
+ context 'when the needed job is optional' do
+ let(:needs_attributes) { [{ name: 'build', optional: true }] }
- it 'does not have errors' do
+ it "does not return an error" do
expect(subject.errors).to be_empty
end
end
+ end
- context 'when using 101 needs' do
- let(:needs_count) { 101 }
-
- 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
+ context 'when build job is part of prior stages' do
+ let(:stage_attributes) do
+ {
+ name: 'build',
+ index: 0,
+ builds: [{ name: 'build' }]
+ }
+ end
- context 'when ci_needs_size_limit is set to 100' do
- before do
- project.actual_limits.update!(ci_needs_size_limit: 100)
- end
+ let(:stage_seed) do
+ Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, [])
+ 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")
- end
- end
+ let(:previous_stages) { [stage_seed] }
- context 'when ci_needs_size_limit is set to 0' do
- before do
- project.actual_limits.update!(ci_needs_size_limit: 0)
- end
+ 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 0 others, but you have listed 101. See needs keyword documentation for more details")
- end
- end
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
end
end
- describe 'applying pipeline variables' do
- subject { seed_build }
+ context 'when build job is part of the same stage' do
+ let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) }
- let(:pipeline_variables) { [] }
- let(:pipeline) do
- build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables)
+ it 'is included' do
+ is_expected.to be_included
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
+ it 'does not have errors' do
+ expect(subject.errors).to be_empty
+ end
+ end
- it "does not have errors" do
- expect(subject.errors).to be_empty
- end
+ context 'when using 101 needs' do
+ let(:needs_count) { 101 }
+
+ 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
- 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')
- ]
+ context 'when ci_needs_size_limit is set to 100' do
+ before do
+ project.actual_limits.update!(ci_needs_size_limit: 100)
end
it "returns an error" do
expect(subject.errors).to contain_exactly(
- 'rspec: circular variable reference detected: ["A", "B", "C"]')
+ "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details")
end
+ 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
+ context 'when ci_needs_size_limit is set to 0' do
+ before do
+ project.actual_limits.update!(ci_needs_size_limit: 0)
+ end
- it "included? returns true" do
- expect(subject.included?).to eq(true)
- 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")
end
end
end
end
- describe 'feature flag ci_reuse_build_in_seed_context' do
- let(:attributes) do
- { name: 'rspec', rules: [{ if: '$VARIABLE == null' }], when: 'on_success' }
+ 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)
end
- context 'when enabled' do
- it_behaves_like 'build seed'
+ 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 'initializes the build once' do
- expect(Ci::Build).to receive(:new).once.and_call_original
- seed_build.to_resource
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
end
end
- context 'when disabled' do
- before do
- stub_feature_flags(ci_reuse_build_in_seed_context: false)
+ 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"]')
end
- it_behaves_like 'build seed'
+ 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 'initializes the build twice' do
- expect(Ci::Build).to receive(:new).twice.and_call_original
- seed_build.to_resource
+ it "included? returns true" do
+ expect(subject.included?).to eq(true)
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
index 68e70525c55..93644aa1497 100644
--- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb
@@ -4,19 +4,25 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
let(:codequality_report) { described_class.new }
- let(:degradation_1) { build(:codequality_degradation_1) }
- let(:degradation_2) { build(:codequality_degradation_2) }
+ let(:degradation_major) { build(:codequality_degradation, :major) }
+ let(:degradation_minor) { build(:codequality_degradation, :minor) }
+ let(:degradation_blocker) { build(:codequality_degradation, :blocker) }
+ let(:degradation_info) { build(:codequality_degradation, :info) }
+ let(:degradation_major_2) { build(:codequality_degradation, :major) }
+ let(:degradation_critical) { build(:codequality_degradation, :critical) }
+ let(:degradation_uppercase_major) { build(:codequality_degradation, severity: 'MAJOR') }
+ let(:degradation_unknown) { build(:codequality_degradation, severity: 'unknown') }
it { expect(codequality_report.degradations).to eq({}) }
describe '#add_degradation' do
context 'when there is a degradation' do
before do
- codequality_report.add_degradation(degradation_1)
+ codequality_report.add_degradation(degradation_major)
end
it 'adds degradation to codequality report' do
- expect(codequality_report.degradations.keys).to eq([degradation_1[:fingerprint]])
+ expect(codequality_report.degradations.keys).to match_array([degradation_major[:fingerprint]])
expect(codequality_report.degradations.values.size).to eq(1)
end
end
@@ -53,8 +59,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
context 'when there are many degradations' do
before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(degradation_major)
+ codequality_report.add_degradation(degradation_minor)
end
it 'returns the number of degradations' do
@@ -68,36 +74,25 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
context 'when there are many degradations' do
before do
- codequality_report.add_degradation(degradation_1)
- codequality_report.add_degradation(degradation_2)
+ codequality_report.add_degradation(degradation_major)
+ codequality_report.add_degradation(degradation_minor)
end
it 'returns all degradations' do
- expect(all_degradations).to contain_exactly(degradation_1, degradation_2)
+ expect(all_degradations).to contain_exactly(degradation_major, degradation_minor)
end
end
end
describe '#sort_degradations!' do
- let(:major) { build(:codequality_degradation, :major) }
- let(:minor) { build(:codequality_degradation, :minor) }
- let(:blocker) { build(:codequality_degradation, :blocker) }
- let(:info) { build(:codequality_degradation, :info) }
- let(:major_2) { build(:codequality_degradation, :major) }
- let(:critical) { build(:codequality_degradation, :critical) }
- let(:uppercase_major) { build(:codequality_degradation, severity: 'MAJOR') }
- let(:unknown) { build(:codequality_degradation, severity: 'unknown') }
-
- let(:codequality_report) { described_class.new }
-
before do
- codequality_report.add_degradation(major)
- codequality_report.add_degradation(minor)
- codequality_report.add_degradation(blocker)
- codequality_report.add_degradation(major_2)
- codequality_report.add_degradation(info)
- codequality_report.add_degradation(critical)
- codequality_report.add_degradation(unknown)
+ codequality_report.add_degradation(degradation_major)
+ codequality_report.add_degradation(degradation_minor)
+ codequality_report.add_degradation(degradation_blocker)
+ codequality_report.add_degradation(degradation_major_2)
+ codequality_report.add_degradation(degradation_info)
+ codequality_report.add_degradation(degradation_critical)
+ codequality_report.add_degradation(degradation_unknown)
codequality_report.sort_degradations!
end
@@ -105,36 +100,70 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
it 'sorts degradations based on severity' do
expect(codequality_report.degradations.values).to eq(
[
- blocker,
- critical,
- major,
- major_2,
- minor,
- info,
- unknown
+ degradation_blocker,
+ degradation_critical,
+ degradation_major,
+ degradation_major_2,
+ degradation_minor,
+ degradation_info,
+ degradation_unknown
])
end
context 'with non-existence and uppercase severities' do
let(:other_report) { described_class.new }
- let(:non_existent) { build(:codequality_degradation, severity: 'non-existent') }
+ let(:degradation_non_existent) { build(:codequality_degradation, severity: 'non-existent') }
before do
- other_report.add_degradation(blocker)
- other_report.add_degradation(uppercase_major)
- other_report.add_degradation(minor)
- other_report.add_degradation(non_existent)
+ other_report.add_degradation(degradation_blocker)
+ other_report.add_degradation(degradation_uppercase_major)
+ other_report.add_degradation(degradation_minor)
+ other_report.add_degradation(degradation_non_existent)
end
it 'sorts unknown last' do
expect(other_report.degradations.values).to eq(
[
- blocker,
- uppercase_major,
- minor,
- non_existent
+ degradation_blocker,
+ degradation_uppercase_major,
+ degradation_minor,
+ degradation_non_existent
])
end
end
end
+
+ describe '#code_quality_report_summary' do
+ context "when there is no degradation" do
+ it 'return nil' do
+ expect(codequality_report.code_quality_report_summary).to eq(nil)
+ end
+ end
+
+ context "when there are degradations" do
+ before do
+ codequality_report.add_degradation(degradation_major)
+ codequality_report.add_degradation(degradation_major_2)
+ codequality_report.add_degradation(degradation_minor)
+ codequality_report.add_degradation(degradation_blocker)
+ codequality_report.add_degradation(degradation_info)
+ codequality_report.add_degradation(degradation_critical)
+ codequality_report.add_degradation(degradation_unknown)
+ end
+
+ it 'returns the summary of the code quality report' do
+ expect(codequality_report.code_quality_report_summary).to eq(
+ {
+ 'major' => 2,
+ 'minor' => 1,
+ 'blocker' => 1,
+ 'info' => 1,
+ 'critical' => 1,
+ 'unknown' => 1,
+ 'count' => 7
+ }
+ )
+ 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 56f69720b87..31c53d4a030 100644
--- a/spec/lib/gitlab/ci/runner_instructions_spec.rb
+++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::RunnerInstructions do
+RSpec.describe Gitlab::Ci::RunnerInstructions, feature_category: :runner_fleet do
using RSpec::Parameterized::TableSyntax
let(:params) { {} }
@@ -29,7 +29,6 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do
context name do
it 'has the required fields' do
expect(subject).to have_key(:human_readable_name)
- expect(subject).to have_key(:installation_instructions_url)
end
end
end
diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb
index ad1e9b12b8a..14f3c95ec79 100644
--- a/spec/lib/gitlab/ci/runner_releases_spec.rb
+++ b/spec/lib/gitlab/ci/runner_releases_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::RunnerReleases do
+RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do
subject { described_class.instance }
let(:runner_releases_url) { 'http://testurl.com/runner_public_releases' }
diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
index 55c3834bfa7..526d6cba657 100644
--- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
+++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
+RSpec.describe Gitlab::Ci::RunnerUpgradeCheck, feature_category: :runner_fleet do
using RSpec::Parameterized::TableSyntax
subject(:instance) { described_class.new(gitlab_version, runner_releases) }
@@ -51,8 +51,8 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
context 'with runner_version from last minor release' do
let(:runner_version) { 'v14.0.1' }
- it 'returns :not_available' do
- is_expected.to eq([parsed_runner_version, :not_available])
+ it 'returns :unavailable' do
+ is_expected.to eq([parsed_runner_version, :unavailable])
end
end
end
@@ -85,8 +85,8 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
context 'with a runner_version that is too recent' do
let(:runner_version) { 'v14.2.0' }
- it 'returns :not_available' do
- is_expected.to eq([parsed_runner_version, :not_available])
+ it 'returns :unavailable' do
+ is_expected.to eq([parsed_runner_version, :unavailable])
end
end
end
@@ -96,14 +96,14 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
context 'with valid params' do
where(:runner_version, :expected_status, :expected_suggested_version) do
- 'v15.0.0' | :not_available | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available
+ 'v15.0.0' | :unavailable | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available
'v14.1.0-rc3' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
'v14.1.0~beta.1574.gf6ea9389' | :recommended | '14.1.1' # suffixes are correctly handled
'v14.1.0/1.1.0' | :recommended | '14.1.1' # suffixes are correctly handled
'v14.1.0' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
'v14.0.1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available
'v14.0.2-rc1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate
- 'v14.0.2' | :not_available | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version
+ 'v14.0.2' | :unavailable | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version
'v13.10.1' | :available | '14.0.2' # available upgrade: 14.0.2
'v13.10.1~beta.1574.gf6ea9389' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available
'v13.10.1/1.1.0' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available
@@ -125,13 +125,13 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
context 'with valid params' do
where(:runner_version, :expected_status, :expected_suggested_version) do
- 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible
- 'v13.10.1' | :not_available | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x
- 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available
- 'v13.9.2' | :not_available | '13.9.2' # not_available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version
- 'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version
- 'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
- 'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
+ 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible
+ 'v13.10.1' | :unavailable | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x
+ 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available
+ 'v13.9.2' | :unavailable | '13.9.2' # not available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version
+ 'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version
+ 'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
+ 'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
index 37524afc83d..fef97c73a91 100644
--- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Bridge::Common do
+RSpec.describe Gitlab::Ci::Status::Bridge::Common, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:bridge) { create(:ci_bridge) }
let_it_be(:downstream_pipeline) { create(:ci_pipeline) }
@@ -37,4 +37,35 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
it { expect(subject.details_path).to be_nil }
end
end
+
+ describe '#label' do
+ let(:description) { 'my description' }
+ let(:bridge) { create(:ci_bridge, description: description) }
+
+ subject do
+ Gitlab::Ci::Status::Created
+ .new(bridge, user)
+ .extend(described_class)
+ end
+
+ it 'returns description' do
+ expect(subject.label).to eq description
+ end
+
+ context 'when description is nil' do
+ let(:description) { nil }
+
+ it 'returns core status label' do
+ expect(subject.label).to eq('created')
+ end
+ end
+
+ context 'when description is empty string' do
+ let(:description) { '' }
+
+ it 'returns core status label' do
+ expect(subject.label).to eq('created')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index c13901a4776..040c3ec7f6e 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
expect(status.text).to eq s_('CiStatusText|created')
expect(status.icon).to eq 'status_created'
expect(status.favicon).to eq 'favicon_status_created'
- expect(status.label).to be_nil
+ expect(status.label).to eq 'created'
expect(status).not_to have_details
expect(status).not_to have_action
end
@@ -40,7 +40,8 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Bridge::Failed]
+ .to eq [Gitlab::Ci::Status::Bridge::Retryable,
+ Gitlab::Ci::Status::Bridge::Failed]
end
it 'fabricates a failed bridge status' do
@@ -51,10 +52,10 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
expect(status.text).to eq s_('CiStatusText|failed')
expect(status.icon).to eq 'status_failed'
expect(status.favicon).to eq 'favicon_status_failed'
- expect(status.label).to be_nil
+ expect(status.label).to eq 'failed'
expect(status.status_tooltip).to eq "#{s_('CiStatusText|failed')} - (unknown failure)"
expect(status).not_to have_details
- expect(status).not_to have_action
+ expect(status).to have_action
end
context 'failed with downstream_pipeline_creation_failed' do
@@ -130,12 +131,36 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
expect(status.text).to eq 'waiting'
expect(status.group).to eq 'waiting-for-resource'
expect(status.icon).to eq 'status_pending'
- expect(status.favicon).to eq 'favicon_pending'
+ expect(status.favicon).to eq 'favicon_status_pending'
expect(status.illustration).to include(:image, :size, :title)
expect(status).not_to have_details
end
end
+ context 'when the bridge is successful and therefore retryable' do
+ let(:bridge) { create(:ci_bridge, :success) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Bridge::Retryable]
+ end
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Bridge::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq s_('CiStatusText|passed')
+ expect(status.icon).to eq 'status_success'
+ expect(status.favicon).to eq 'favicon_status_success'
+ expect(status).to have_action
+ end
+ end
+
private
def create_bridge(*traits)
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index ade07a54877..2c93f842a30 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_button_title' do
- it { expect(subject.action_button_title).to eq 'Trigger this manual action' }
+ it { expect(subject.action_button_title).to eq 'Run job' }
end
describe '.matches?' do
diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
index bb6139accaf..6f5ab77a358 100644
--- a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
+++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do
end
describe '#favicon' do
- it { expect(subject.favicon).to eq 'favicon_pending' }
+ it { expect(subject.favicon).to eq 'favicon_status_pending' }
end
describe '#group' do
diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
index 43deb465025..e4cee379f40 100644
--- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do
+RSpec.describe '5-Minute-Production-App.gitlab-ci.yml', feature_category: :five_minute_production_app do
subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('5-Minute-Production-App') }
describe 'the created pipeline' do
- let_it_be(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
+ let_it_be_with_refind(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
- let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) }
let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
@@ -24,24 +24,27 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do
end
context 'when AWS variables are set' do
+ def create_ci_variable(key, value)
+ create(:ci_variable, project: project, key: key, value: value)
+ end
+
before do
- create(:ci_variable, project: project, key: 'AWS_ACCESS_KEY_ID', value: 'AKIAIOSFODNN7EXAMPLE')
- create(:ci_variable, project: project, key: 'AWS_SECRET_ACCESS_KEY', value: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY')
- create(:ci_variable, project: project, key: 'AWS_DEFAULT_REGION', value: 'us-west-2')
+ create_ci_variable('AWS_ACCESS_KEY_ID', 'AKIAIOSFODNN7EXAMPLE')
+ create_ci_variable('AWS_SECRET_ACCESS_KEY', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY')
+ create_ci_variable('AWS_DEFAULT_REGION', 'us-west-2')
end
it 'creates all jobs' do
- expect(build_names).to match_array(%w(build terraform_apply deploy terraform_destroy))
+ expect(build_names).to match_array(%w[build terraform_apply deploy terraform_destroy])
end
- context 'pipeline branch is protected' do
+ context 'when pipeline branch is protected' do
before do
create(:protected_branch, project: project, name: pipeline_branch)
- project.reload
end
it 'does not create a destroy job' do
- expect(build_names).to match_array(%w(build terraform_apply deploy))
+ expect(build_names).to match_array(%w[build terraform_apply deploy])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..9f4f6b02b0b
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform/Module-Base.gitlab-ci.yml', feature_category: :continuous_integration do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Module-Base') }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { 'main' }
+ let(:pipeline_branch) { default_branch }
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let(:user) { project.first_owner }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) }
+ let(:pipeline) { service.execute(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ it 'does not create any jobs' do
+ expect(build_names).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..7c3c1776111
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Terraform-Module.gitlab-ci.yml', feature_category: :continuous_integration do
+ before do
+ allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([])
+ end
+
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform-Module') }
+
+ shared_examples 'on any branch' do
+ it 'creates fmt and kics job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).to include('fmt', 'kics-iac-sast')
+ end
+
+ it 'does not create a deploy job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).not_to include('deploy')
+ end
+ end
+
+ let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1', create_tag: '1.0.0') }
+ let_it_be(:user) { project.first_owner }
+
+ describe 'the created pipeline' do
+ let(:default_branch) { project.default_branch_or_main }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute(:push).payload }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_next_instance_of(Ci::BuildScheduleWorker) do |instance|
+ allow(instance).to receive(:perform).and_return(true)
+ end
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'when on default branch' do
+ let(:pipeline_ref) { default_branch }
+
+ it_behaves_like 'on any branch'
+ end
+
+ context 'when outside the default branch' do
+ let(:pipeline_ref) { 'patch-1' }
+
+ it_behaves_like 'on any branch'
+ end
+
+ context 'when on tag' do
+ let(:pipeline_ref) { '1.0.0' }
+
+ it 'creates fmt and deploy job', :aggregate_failures do
+ expect(pipeline.errors).to be_empty
+ expect(build_names).to include('fmt', 'deploy')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb
index 582c4ad343f..cce6477b91e 100644
--- a/spec/lib/gitlab/ci/trace/archive_spec.rb
+++ b/spec/lib/gitlab/ci/trace/archive_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Trace::Archive do
+RSpec.describe Gitlab::Ci::Trace::Archive, feature_category: :scalability do
context 'with transactional fixtures' do
let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live) }
let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) }
diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb
new file mode 100644
index 00000000000..a5365ae53b8
--- /dev/null
+++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb
@@ -0,0 +1,336 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipeline_authoring do
+ let_it_be(:project) { create_default(:project, :repository, create_tag: 'test').freeze }
+ let_it_be(:user) { create(:user) }
+
+ let(:pipeline) { build(:ci_empty_pipeline, :created, project: project) }
+
+ describe '#predefined_variables' do
+ subject { described_class.new(pipeline).predefined_variables }
+
+ it 'includes all predefined variables in a valid order' do
+ keys = subject.pluck(:key)
+
+ expect(keys).to contain_exactly(*%w[
+ CI_PIPELINE_IID
+ CI_PIPELINE_SOURCE
+ CI_PIPELINE_CREATED_AT
+ CI_COMMIT_SHA
+ CI_COMMIT_SHORT_SHA
+ CI_COMMIT_BEFORE_SHA
+ CI_COMMIT_REF_NAME
+ CI_COMMIT_REF_SLUG
+ CI_COMMIT_BRANCH
+ CI_COMMIT_MESSAGE
+ CI_COMMIT_TITLE
+ CI_COMMIT_DESCRIPTION
+ CI_COMMIT_REF_PROTECTED
+ CI_COMMIT_TIMESTAMP
+ CI_COMMIT_AUTHOR
+ CI_BUILD_REF
+ CI_BUILD_BEFORE_SHA
+ CI_BUILD_REF_NAME
+ CI_BUILD_REF_SLUG
+ ])
+ end
+
+ context 'when the pipeline is running for a tag' do
+ let(:pipeline) { build(:ci_empty_pipeline, :created, project: project, ref: 'test', tag: true) }
+
+ it 'includes all predefined variables in a valid order' do
+ keys = subject.pluck(:key)
+
+ expect(keys).to contain_exactly(*%w[
+ CI_PIPELINE_IID
+ CI_PIPELINE_SOURCE
+ CI_PIPELINE_CREATED_AT
+ CI_COMMIT_SHA
+ CI_COMMIT_SHORT_SHA
+ CI_COMMIT_BEFORE_SHA
+ CI_COMMIT_REF_NAME
+ CI_COMMIT_REF_SLUG
+ CI_COMMIT_MESSAGE
+ CI_COMMIT_TITLE
+ CI_COMMIT_DESCRIPTION
+ CI_COMMIT_REF_PROTECTED
+ CI_COMMIT_TIMESTAMP
+ CI_COMMIT_AUTHOR
+ CI_BUILD_REF
+ CI_BUILD_BEFORE_SHA
+ CI_BUILD_REF_NAME
+ CI_BUILD_REF_SLUG
+ CI_COMMIT_TAG
+ CI_COMMIT_TAG_MESSAGE
+ CI_BUILD_TAG
+ ])
+ end
+ end
+
+ context 'when merge request is present' do
+ let_it_be(:assignees) { create_list(:user, 2) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:labels) { create_list(:label, 2) }
+
+ let(:merge_request) do
+ create(:merge_request, :simple,
+ source_project: project,
+ target_project: project,
+ assignees: assignees,
+ milestone: milestone,
+ labels: labels)
+ end
+
+ context 'when pipeline for merge request is created' do
+ let(:pipeline) do
+ create(:ci_pipeline, :detached_merge_request_pipeline,
+ ci_ref_presence: false,
+ user: user,
+ merge_request: merge_request)
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'exposes merge request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
+ 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
+ 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
+ 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(
+ merge_request.target_project,
+ merge_request.target_branch
+ ).to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '',
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '',
+ 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
+ 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
+ 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
+ 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
+ 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached',
+ 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
+ end
+
+ it 'exposes diff variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
+ 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
+ end
+
+ context 'without assignee' do
+ let(:assignees) { [] }
+
+ it 'does not expose assignee variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
+ end
+ end
+
+ context 'without milestone' do
+ let(:milestone) { nil }
+
+ it 'does not expose milestone variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
+ end
+ end
+
+ context 'without labels' do
+ let(:labels) { [] }
+
+ it 'does not expose labels variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
+ end
+ end
+ end
+
+ context 'when pipeline on branch is created' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, user: user, ref: 'feature')
+ end
+
+ context 'when a merge request is created' do
+ before do
+ merge_request
+ end
+
+ context 'when user has access to project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'merge request references are returned matching the pipeline' do
+ expect(subject.to_hash).to include(
+ 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
+ end
+ end
+
+ context 'when user does not have access to project' do
+ it 'CI_OPEN_MERGE_REQUESTS is not returned' do
+ expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
+ end
+ end
+ end
+
+ context 'when no a merge request is created' do
+ it 'CI_OPEN_MERGE_REQUESTS is not returned' do
+ expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
+ end
+ end
+ end
+
+ context 'with merged results' do
+ let(:pipeline) do
+ create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request)
+ end
+
+ it 'exposes merge request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
+ 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
+ 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
+ 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
+ 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(
+ merge_request.target_project,
+ merge_request.target_branch
+ ).to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
+ 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
+ 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
+ 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
+ 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
+ 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
+ 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result')
+ end
+
+ it 'exposes diff variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
+ 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
+ end
+ end
+ end
+
+ context 'when source is external pull request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
+ end
+
+ let(:pull_request) { create(:external_pull_request, project: project) }
+
+ it 'exposes external pull request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
+ )
+ end
+ end
+
+ describe 'variable CI_KUBERNETES_ACTIVE' do
+ context 'when pipeline.has_kubernetes_active? is true' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
+ end
+
+ it "is included with value 'true'" do
+ expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
+ end
+ end
+
+ context 'when pipeline.has_kubernetes_active? is false' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
+ end
+
+ it 'is not included' do
+ expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
+ end
+ end
+ end
+
+ describe 'variable CI_GITLAB_FIPS_MODE' do
+ context 'when FIPS flag is enabled' do
+ before do
+ allow(Gitlab::FIPS).to receive(:enabled?).and_return(true)
+ end
+
+ it "is included with value 'true'" do
+ expect(subject.to_hash).to include('CI_GITLAB_FIPS_MODE' => 'true')
+ end
+ end
+
+ context 'when FIPS flag is disabled' do
+ before do
+ allow(Gitlab::FIPS).to receive(:enabled?).and_return(false)
+ end
+
+ it 'is not included' do
+ expect(subject.to_hash).not_to have_key('CI_GITLAB_FIPS_MODE')
+ end
+ end
+ end
+
+ context 'when tag is not found' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, ref: 'not_found_tag', tag: true)
+ end
+
+ it 'does not expose tag variables' do
+ expect(subject.to_hash.keys)
+ .not_to include(
+ 'CI_COMMIT_TAG',
+ 'CI_COMMIT_TAG_MESSAGE',
+ 'CI_BUILD_TAG'
+ )
+ end
+ end
+
+ context 'without a commit' do
+ let(:pipeline) { build(:ci_empty_pipeline, :created, sha: nil) }
+
+ it 'does not expose commit variables' do
+ expect(subject.to_hash.keys)
+ .not_to include(
+ 'CI_COMMIT_SHA',
+ 'CI_COMMIT_SHORT_SHA',
+ 'CI_COMMIT_BEFORE_SHA',
+ 'CI_COMMIT_REF_NAME',
+ 'CI_COMMIT_REF_SLUG',
+ 'CI_COMMIT_BRANCH',
+ 'CI_COMMIT_TAG',
+ 'CI_COMMIT_MESSAGE',
+ 'CI_COMMIT_TITLE',
+ 'CI_COMMIT_DESCRIPTION',
+ 'CI_COMMIT_REF_PROTECTED',
+ 'CI_COMMIT_TIMESTAMP',
+ 'CI_COMMIT_AUTHOR')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index 5aa752ee429..bbd3dc54e6a 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -166,8 +166,14 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur
end
before do
+ pipeline_variables_builder = double(
+ ::Gitlab::Ci::Variables::Builder::Pipeline,
+ predefined_variables: [var('C', 3), var('D', 3)]
+ )
+
allow(builder).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] }
allow(pipeline.project).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] }
+ allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder }
allow(pipeline).to receive(:predefined_variables) { [var('C', 3), var('D', 3)] }
allow(job).to receive(:runner) { double(predefined_variables: [var('D', 4), var('E', 4)]) }
allow(builder).to receive(:kubernetes_variables) { [var('E', 5), var('F', 5)] }
@@ -635,8 +641,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur
end
before do
+ pipeline_variables_builder = double(
+ ::Gitlab::Ci::Variables::Builder::Pipeline,
+ predefined_variables: [var('B', 2), var('C', 2)]
+ )
+
allow(pipeline.project).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] }
- allow(pipeline).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] }
+ allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder }
allow(builder).to receive(:secret_instance_variables) { [var('C', 3), var('D', 3)] }
allow(builder).to receive(:secret_group_variables) { [var('D', 4), var('E', 4)] }
allow(builder).to receive(:secret_project_variables) { [var('E', 5), var('F', 5)] }
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index b9f65ff749d..360686ce65c 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1503,7 +1503,7 @@ module Gitlab
end
context "when the included internal file is not present" do
- it_behaves_like 'returns errors', "Local file `/local.gitlab-ci.yml` does not exist!"
+ it_behaves_like 'returns errors', "Local file `local.gitlab-ci.yml` does not exist!"
end
end
end
diff --git a/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
new file mode 100644
index 00000000000..bae98f9bc35
--- /dev/null
+++ b/spec/lib/gitlab/config/loader/multi_doc_yaml_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Loader::MultiDocYaml, feature_category: :pipeline_authoring do
+ let(:loader) { described_class.new(yml, max_documents: 2) }
+
+ describe '#load!' do
+ let(:yml) do
+ <<~YAML
+ spec:
+ inputs:
+ test_input:
+ ---
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'returns the loaded YAML with all keys as symbols' do
+ expect(loader.load!).to eq([
+ { spec: { inputs: { test_input: nil } } },
+ { test_job: { script: 'echo "$[[ inputs.test_input ]]"' } }
+ ])
+ end
+
+ context 'when the YAML file is empty' do
+ let(:yml) { '' }
+
+ it 'returns an empty array' do
+ expect(loader.load!).to be_empty
+ end
+ end
+
+ context 'when the parsed YAML is too big' do
+ let(:yml) do
+ <<~YAML
+ a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
+ b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
+ c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
+ d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
+ e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
+ f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
+ g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
+ h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
+ i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+ ---
+ a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
+ b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
+ c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
+ d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
+ e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
+ f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
+ g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
+ h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
+ i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
+ YAML
+ end
+
+ it 'raises a DataTooLargeError' do
+ expect { loader.load! }.to raise_error(described_class::DataTooLargeError, 'The parsed YAML is too big')
+ end
+ end
+
+ context 'when a document is not a hash' do
+ let(:yml) do
+ <<~YAML
+ not_a_hash
+ ---
+ test_job:
+ script: echo "$[[ inputs.test_input ]]"
+ YAML
+ end
+
+ it 'raises a NotHashError' do
+ expect { loader.load! }.to raise_error(described_class::NotHashError, 'Invalid configuration format')
+ end
+ end
+
+ context 'when there are too many documents' do
+ let(:yml) do
+ <<~YAML
+ a: b
+ ---
+ c: d
+ ---
+ e: f
+ YAML
+ end
+
+ it 'raises a TooManyDocumentsError' do
+ expect { loader.load! }.to raise_error(
+ described_class::TooManyDocumentsError,
+ 'The parsed YAML has too many documents'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index c7f84cd583c..346424d1681 100644
--- a/spec/lib/gitlab/config/loader/yaml_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Config::Loader::Yaml do
+RSpec.describe Gitlab::Config::Loader::Yaml, feature_category: :pipeline_authoring do
let(:loader) { described_class.new(yml) }
let(:yml) do
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 963c9fe1576..c962b9ad393 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
context 'with a multiple database' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
context 'when both databases meets minimum supported version' do
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 88bffd41947..f298890623f 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -94,13 +94,31 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
it 'adds CDN host to CSP' do
expect(directives['script_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.script_src + " https://cdn.example.com")
- expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://cdn.example.com")
+ expect(directives['style_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.style_src + " https://cdn.example.com")
expect(directives['font_src']).to eq("'self' https://cdn.example.com")
expect(directives['worker_src']).to eq('http://localhost/assets/ blob: data: https://cdn.example.com')
expect(directives['frame_src']).to eq(::Gitlab::ContentSecurityPolicy::Directives.frame_src + " https://cdn.example.com http://localhost/admin/ http://localhost/assets/ http://localhost/-/speedscope/index.html http://localhost/-/sandbox/")
end
end
+ describe 'Zuora directives' do
+ context 'when is Gitlab.com?' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'adds Zuora host to CSP' do
+ expect(directives['frame_src']).to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
+ end
+ end
+
+ context 'when is not Gitlab.com?' do
+ it 'does not add Zuora host to CSP' do
+ expect(directives['frame_src']).not_to include('https://*.zuora.com/apps/PublicHostedPageLite.do')
+ end
+ end
+ 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' }
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 3736914669a..326e27fa716 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ContributionsCalendar, feature_category: :users do
+RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do
let_it_be_with_reload(:contributor) { create(:user) }
let_it_be_with_reload(:user) { create(:user) }
let(:travel_time) { nil }
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 92ffeee8509..f85fb1540d9 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::CycleAnalytics::StageSummary do
+RSpec.describe Gitlab::CycleAnalytics::StageSummary, feature_category: :devops_reports do
include CycleAnalyticsHelpers
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/lib/gitlab/database/indexing_exclusive_lease_guard_spec.rb b/spec/lib/gitlab/database/async_ddl_exclusive_lease_guard_spec.rb
index ddc9cdee92f..60ccf5ec685 100644
--- a/spec/lib/gitlab/database/indexing_exclusive_lease_guard_spec.rb
+++ b/spec/lib/gitlab/database/async_ddl_exclusive_lease_guard_spec.rb
@@ -2,25 +2,25 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::IndexingExclusiveLeaseGuard, feature_category: :database do
+RSpec.describe Gitlab::Database::AsyncDdlExclusiveLeaseGuard, feature_category: :database do
let(:helper_class) do
Class.new do
- include Gitlab::Database::IndexingExclusiveLeaseGuard
+ include Gitlab::Database::AsyncDdlExclusiveLeaseGuard
- attr_reader :connection
+ attr_reader :connection_db_config
- def initialize(connection)
- @connection = connection
+ def initialize(connection_db_config)
+ @connection_db_config = connection_db_config
end
end
end
describe '#lease_key' do
- let(:helper) { helper_class.new(connection) }
- let(:lease_key) { "gitlab/database/indexing/actions/#{database_name}" }
+ let(:helper) { helper_class.new(connection_db_config) }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{database_name}" }
context 'with CI database connection' do
- let(:connection) { Ci::ApplicationRecord.connection }
+ let(:connection_db_config) { Ci::ApplicationRecord.connection_db_config }
let(:database_name) { Gitlab::Database::CI_DATABASE_NAME }
before do
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::Database::IndexingExclusiveLeaseGuard, feature_category:
end
context 'with MAIN database connection' do
- let(:connection) { ApplicationRecord.connection }
+ let(:connection_db_config) { ApplicationRecord.connection_db_config }
let(:database_name) { Gitlab::Database::MAIN_DATABASE_NAME }
it { expect(helper.lease_key).to eq(lease_key) }
diff --git a/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb
new file mode 100644
index 00000000000..90137e259f5
--- /dev/null
+++ b/spec/lib/gitlab/database/async_foreign_keys/foreign_key_validator_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncForeignKeys::ForeignKeyValidator, feature_category: :database do
+ include ExclusiveLeaseHelpers
+
+ describe '#perform' do
+ let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
+
+ let(:fk_model) { Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation }
+ let(:table_name) { '_test_async_fks' }
+ let(:fk_name) { 'fk_parent_id' }
+ let(:validation) { create(:postgres_async_foreign_key_validation, table_name: table_name, name: fk_name) }
+ let(:connection) { validation.connection }
+
+ subject { described_class.new(validation) }
+
+ before do
+ connection.create_table(table_name) do |t|
+ t.references :parent, foreign_key: { to_table: table_name, validate: false, name: fk_name }
+ end
+ end
+
+ it 'validates the FK while controlling statement timeout' do
+ allow(connection).to receive(:execute).and_call_original
+ expect(connection).to receive(:execute)
+ .with("SET statement_timeout TO '43200s'").ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with('ALTER TABLE "_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";').ordered.and_call_original
+ expect(connection).to receive(:execute)
+ .with("RESET statement_timeout").ordered.and_call_original
+
+ subject.perform
+ end
+
+ context 'with fully qualified table names' do
+ let(:validation) do
+ create(:postgres_async_foreign_key_validation,
+ table_name: "public.#{table_name}",
+ name: fk_name
+ )
+ end
+
+ it 'validates the FK' do
+ allow(connection).to receive(:execute).and_call_original
+
+ expect(connection).to receive(:execute)
+ .with('ALTER TABLE "public"."_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";').ordered.and_call_original
+
+ subject.perform
+ end
+ end
+
+ it 'removes the FK validation record from table' do
+ expect(validation).to receive(:destroy!).and_call_original
+
+ expect { subject.perform }.to change { fk_model.count }.by(-1)
+ end
+
+ it 'skips logic if not able to acquire exclusive lease' do
+ expect(lease).to receive(:try_obtain).ordered.and_return(false)
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+ expect(validation).not_to receive(:destroy!)
+
+ expect { subject.perform }.not_to change { fk_model.count }
+ end
+
+ it 'logs messages around execution' do
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Starting to validate foreign key'))
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Finished validating foreign key'))
+ end
+
+ context 'when the FK does not exist' do
+ before do
+ connection.create_table(table_name, force: true)
+ end
+
+ it 'skips validation and removes the record' do
+ expect(connection).not_to receive(:execute).with(/ALTER TABLE/)
+
+ expect { subject.perform }.to change { fk_model.count }.by(-1)
+ end
+
+ it 'logs an appropriate message' do
+ expected_message = "Skipping #{fk_name} validation since it does not exist. The queuing entry will be deleted"
+
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: expected_message))
+ end
+ end
+
+ context 'with error handling' do
+ before do
+ allow(connection).to receive(:execute).and_call_original
+
+ allow(connection).to receive(:execute)
+ .with('ALTER TABLE "_test_async_fks" VALIDATE CONSTRAINT "fk_parent_id";')
+ .and_raise(ActiveRecord::StatementInvalid)
+ end
+
+ context 'on production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ it 'increases execution attempts' do
+ expect { subject.perform }.to change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+
+ it 'logs an error message including the fk_name' do
+ expect(Gitlab::AppLogger)
+ .to receive(:error)
+ .with(a_hash_including(:message, :fk_name))
+ .and_call_original
+
+ subject.perform
+ end
+ end
+
+ context 'on development' do
+ it 'also raises errors' do
+ expect { subject.perform }
+ .to raise_error(ActiveRecord::StatementInvalid)
+ .and change { validation.attempts }.by(1)
+
+ expect(validation.last_error).to be_present
+ expect(validation).not_to be_destroyed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb
new file mode 100644
index 00000000000..0bd0e8045ff
--- /dev/null
+++ b/spec/lib/gitlab/database/async_foreign_keys/migration_helpers_spec.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncForeignKeys::MigrationHelpers, feature_category: :database do
+ let(:migration) { Gitlab::Database::Migration[2.1].new }
+ let(:connection) { ApplicationRecord.connection }
+ let(:fk_model) { Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation }
+ let(:table_name) { '_test_async_fks' }
+ let(:column_name) { 'parent_id' }
+ let(:fk_name) { nil }
+
+ context 'with regular tables' do
+ before do
+ allow(migration).to receive(:puts)
+ allow(migration.connection).to receive(:transaction_open?).and_return(false)
+
+ connection.create_table(table_name) do |t|
+ t.integer column_name
+ end
+
+ migration.add_concurrent_foreign_key(
+ table_name, table_name,
+ column: column_name, validate: false, name: fk_name)
+ end
+
+ describe '#prepare_async_foreign_key_validation' do
+ it 'creates the record for the async FK validation' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, column_name)
+ end.to change { fk_model.where(table_name: table_name).count }.by(1)
+
+ record = fk_model.find_by(table_name: table_name)
+
+ expect(record.name).to start_with('fk_')
+ end
+
+ context 'when an explicit name is given' do
+ let(:fk_name) { 'my_fk_name' }
+
+ it 'creates the record with the given name' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.to change { fk_model.where(name: fk_name).count }.by(1)
+
+ record = fk_model.find_by(name: fk_name)
+
+ expect(record.table_name).to eq(table_name)
+ end
+ end
+
+ context 'when the FK does not exist' do
+ it 'returns an error' do
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: 'no_fk')
+ end.to raise_error RuntimeError, /Could not find foreign key "no_fk" on table "_test_async_fks"/
+ end
+ end
+
+ context 'when the record already exists' do
+ let(:fk_name) { 'my_fk_name' }
+
+ it 'does attempt to create the record' do
+ create(:postgres_async_foreign_key_validation, table_name: table_name, name: fk_name)
+
+ expect do
+ migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.not_to change { fk_model.where(name: fk_name).count }
+ end
+ end
+
+ context 'when the async FK validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(:postgres_async_foreign_key_validations)
+
+ expect(fk_model).not_to receive(:safe_find_or_create_by!)
+
+ expect { migration.prepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#unprepare_async_foreign_key_validation' do
+ before do
+ migration.prepare_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_foreign_key_validation(table_name, column_name)
+ end.to change { fk_model.where(table_name: table_name).count }.by(-1)
+ end
+
+ context 'when an explicit name is given' do
+ let(:fk_name) { 'my_test_async_fk' }
+
+ it 'destroys the record' do
+ expect do
+ migration.unprepare_async_foreign_key_validation(table_name, name: fk_name)
+ end.to change { fk_model.where(name: fk_name).count }.by(-1)
+ end
+ end
+
+ context 'when the async fk validation table does not exist' do
+ it 'does not raise an error' do
+ connection.drop_table(:postgres_async_foreign_key_validations)
+
+ expect(fk_model).not_to receive(:find_by)
+
+ expect { migration.unprepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ context 'with partitioned tables' do
+ let(:partition_schema) { 'gitlab_partitions_dynamic' }
+ let(:partition1_name) { "#{partition_schema}.#{table_name}_202001" }
+ let(:partition2_name) { "#{partition_schema}.#{table_name}_202002" }
+ let(:fk_name) { 'my_partitioned_fk_name' }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id serial NOT NULL,
+ #{column_name} int NOT NULL,
+ created_at timestamptz NOT NULL,
+ PRIMARY KEY (id, created_at)
+ ) PARTITION BY RANGE (created_at);
+
+ CREATE TABLE #{partition1_name} PARTITION OF #{table_name}
+ FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
+
+ CREATE TABLE #{partition2_name} PARTITION OF #{table_name}
+ FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
+ SQL
+ end
+
+ describe '#prepare_partitioned_async_foreign_key_validation' do
+ it 'delegates to prepare_async_foreign_key_validation for each partition' do
+ expect(migration)
+ .to receive(:prepare_async_foreign_key_validation)
+ .with(partition1_name, column_name, name: fk_name)
+
+ expect(migration)
+ .to receive(:prepare_async_foreign_key_validation)
+ .with(partition2_name, column_name, name: fk_name)
+
+ migration.prepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+ end
+
+ describe '#unprepare_partitioned_async_foreign_key_validation' do
+ it 'delegates to unprepare_async_foreign_key_validation for each partition' do
+ expect(migration)
+ .to receive(:unprepare_async_foreign_key_validation)
+ .with(partition1_name, column_name, name: fk_name)
+
+ expect(migration)
+ .to receive(:unprepare_async_foreign_key_validation)
+ .with(partition2_name, column_name, name: fk_name)
+
+ migration.unprepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb
new file mode 100644
index 00000000000..ba201d93f52
--- /dev/null
+++ b/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValidation, type: :model,
+ feature_category: :database do
+ it { is_expected.to be_a Gitlab::Database::SharedModel }
+
+ describe 'validations' do
+ let_it_be(:fk_validation) { create(:postgres_async_foreign_key_validation) }
+ let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
+ let(:last_error_limit) { described_class::MAX_LAST_ERROR_LENGTH }
+
+ subject { fk_validation }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_presence_of(:table_name) }
+ it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) }
+ it { is_expected.to validate_length_of(:last_error).is_at_most(last_error_limit) }
+ end
+
+ describe 'scopes' do
+ let!(:failed_validation) { create(:postgres_async_foreign_key_validation, attempts: 1) }
+ let!(:new_validation) { create(:postgres_async_foreign_key_validation) }
+
+ describe '.ordered' do
+ subject { described_class.ordered }
+
+ it { is_expected.to eq([new_validation, failed_validation]) }
+ end
+ end
+
+ describe '#handle_exception!' do
+ let_it_be_with_reload(:fk_validation) { create(:postgres_async_foreign_key_validation) }
+
+ let(:error) { instance_double(StandardError, message: 'Oups', backtrace: %w[this that]) }
+
+ subject { fk_validation.handle_exception!(error) }
+
+ it 'increases the attempts number' do
+ expect { subject }.to change { fk_validation.reload.attempts }.by(1)
+ end
+
+ it 'saves error details' do
+ subject
+
+ expect(fk_validation.reload.last_error).to eq("Oups\nthis\nthat")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_foreign_keys_spec.rb b/spec/lib/gitlab/database/async_foreign_keys_spec.rb
new file mode 100644
index 00000000000..f15eb364929
--- /dev/null
+++ b/spec/lib/gitlab/database/async_foreign_keys_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncForeignKeys, feature_category: :database do
+ describe '.validate_pending_entries!' do
+ subject { described_class.validate_pending_entries! }
+
+ before do
+ create_list(:postgres_async_foreign_key_validation, 3)
+ end
+
+ it 'takes 2 pending FK validations and executes them' do
+ validations = described_class::PostgresAsyncForeignKeyValidation.ordered.limit(2).to_a
+
+ expect_next_instances_of(described_class::ForeignKeyValidator, 2, validations) do |validator|
+ expect(validator).to receive(:perform)
+ end
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes/index_base_spec.rb b/spec/lib/gitlab/database/async_indexes/index_base_spec.rb
new file mode 100644
index 00000000000..d6070ff215e
--- /dev/null
+++ b/spec/lib/gitlab/database/async_indexes/index_base_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::AsyncIndexes::IndexBase, feature_category: :database do
+ describe '#perform' do
+ subject { described_class.new(async_index) }
+
+ let(:async_index) { create(:postgres_async_index) }
+ let(:model) { Gitlab::Database.database_base_models[Gitlab::Database::PRIMARY_DATABASE_NAME] }
+ let(:connection) { model.connection }
+
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
+ end
+
+ describe '#preconditions_met?' do
+ it 'raises errors if preconditions is not defined' do
+ expect { subject.perform }.to raise_error NotImplementedError, 'must implement preconditions_met?'
+ end
+ end
+
+ describe '#action_type' do
+ before do
+ allow(subject).to receive(:preconditions_met?).and_return(true)
+ end
+
+ it 'raises errors if action_type is not defined' do
+ expect { subject.perform }.to raise_error NotImplementedError, 'must implement action_type'
+ end
+ end
+
+ context 'with error handling' do
+ before do
+ allow(subject).to receive(:preconditions_met?).and_return(true)
+ allow(subject).to receive(:action_type).and_return('test')
+ allow(async_index.connection).to receive(:execute).and_call_original
+
+ allow(async_index.connection)
+ .to receive(:execute)
+ .with(async_index.definition)
+ .and_raise(ActiveRecord::StatementInvalid)
+ end
+
+ context 'on production' do
+ before do
+ allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false)
+ end
+
+ it 'increases execution attempts' do
+ expect { subject.perform }.to change { async_index.attempts }.by(1)
+
+ expect(async_index.last_error).to be_present
+ expect(async_index).not_to be_destroyed
+ end
+
+ it 'logs an error message including the index_name' do
+ expect(Gitlab::AppLogger)
+ .to receive(:error)
+ .with(a_hash_including(:message, :index_name))
+ .and_call_original
+
+ subject.perform
+ end
+ end
+
+ context 'on development' do
+ it 'also raises errors' do
+ expect { subject.perform }
+ .to raise_error(ActiveRecord::StatementInvalid)
+ .and change { async_index.attempts }.by(1)
+
+ expect(async_index.last_error).to be_present
+ expect(async_index).not_to be_destroyed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
index 207aedd1a38..51a09ba0b5e 100644
--- a/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
+RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator, feature_category: :database do
include ExclusiveLeaseHelpers
describe '#perform' do
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
let(:connection) { model.connection }
let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
- let(:lease_key) { "gitlab/database/indexing/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
around do |example|
@@ -35,6 +35,24 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
subject.perform
end
+
+ it 'removes the index preparation record from postgres_async_indexes' do
+ expect(async_index).to receive(:destroy!).and_call_original
+
+ expect { subject.perform }.to change { index_model.count }.by(-1)
+ end
+
+ it 'logs an appropriate message' do
+ expected_message = 'Skipping index creation since preconditions are not met. The queuing entry will be deleted'
+
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: expected_message))
+ end
end
it 'creates the index while controlling statement timeout' do
@@ -47,7 +65,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
end
it 'removes the index preparation record from postgres_async_indexes' do
- expect(async_index).to receive(:destroy).and_call_original
+ expect(async_index).to receive(:destroy!).and_call_original
expect { subject.perform }.to change { index_model.count }.by(-1)
end
@@ -55,9 +73,23 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
it 'skips logic if not able to acquire exclusive lease' do
expect(lease).to receive(:try_obtain).ordered.and_return(false)
expect(connection).not_to receive(:execute).with(/CREATE INDEX/)
- expect(async_index).not_to receive(:destroy)
+ expect(async_index).not_to receive(:destroy!)
expect { subject.perform }.not_to change { index_model.count }
end
+
+ it 'logs messages around execution' do
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Starting async index creation'))
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Finished async index creation'))
+ end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb b/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb
index 11039ad4f7e..7f0febdcacd 100644
--- a/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/index_destructor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do
+RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor, feature_category: :database do
include ExclusiveLeaseHelpers
describe '#perform' do
@@ -16,7 +16,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do
let(:connection) { model.connection }
let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) }
- let(:lease_key) { "gitlab/database/indexing/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION }
before do
@@ -39,6 +39,24 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do
subject.perform
end
+
+ it 'removes the index preparation record from postgres_async_indexes' do
+ expect(async_index).to receive(:destroy!).and_call_original
+
+ expect { subject.perform }.to change { index_model.count }.by(-1)
+ end
+
+ it 'logs an appropriate message' do
+ expected_message = 'Skipping index removal since preconditions are not met. The queuing entry will be deleted'
+
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: expected_message))
+ end
end
it 'creates the index while controlling lock timeout' do
@@ -53,7 +71,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do
end
it 'removes the index preparation record from postgres_async_indexes' do
- expect(async_index).to receive(:destroy).and_call_original
+ expect(async_index).to receive(:destroy!).and_call_original
expect { subject.perform }.to change { index_model.count }.by(-1)
end
@@ -61,9 +79,23 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexDestructor do
it 'skips logic if not able to acquire exclusive lease' do
expect(lease).to receive(:try_obtain).ordered.and_return(false)
expect(connection).not_to receive(:execute).with(/DROP INDEX/)
- expect(async_index).not_to receive(:destroy)
+ expect(async_index).not_to receive(:destroy!)
expect { subject.perform }.not_to change { index_model.count }
end
+
+ it 'logs messages around execution' do
+ allow(Gitlab::AppLogger).to receive(:info).and_call_original
+
+ subject.perform
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Starting async index removal'))
+
+ expect(Gitlab::AppLogger)
+ .to have_received(:info)
+ .with(a_hash_including(message: 'Finished async index removal'))
+ end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
index 52f5e37eff2..7c5c368fcb5 100644
--- a/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/migration_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::AsyncIndexes::MigrationHelpers do
+RSpec.describe Gitlab::Database::AsyncIndexes::MigrationHelpers, feature_category: :database do
let(:migration) { ActiveRecord::Migration.new.extend(described_class) }
let(:index_model) { Gitlab::Database::AsyncIndexes::PostgresAsyncIndex }
let(:connection) { ApplicationRecord.connection }
diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
index 806d57af4b3..5e9d4f78a4a 100644
--- a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb
@@ -2,12 +2,13 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model do
+RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model, feature_category: :database do
it { is_expected.to be_a Gitlab::Database::SharedModel }
describe 'validations' do
let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH }
let(:definition_limit) { described_class::MAX_DEFINITION_LENGTH }
+ let(:last_error_limit) { described_class::MAX_LAST_ERROR_LENGTH }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) }
@@ -15,11 +16,12 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model
it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) }
it { is_expected.to validate_presence_of(:definition) }
it { is_expected.to validate_length_of(:definition).is_at_most(definition_limit) }
+ it { is_expected.to validate_length_of(:last_error).is_at_most(last_error_limit) }
end
describe 'scopes' do
- let!(:async_index_creation) { create(:postgres_async_index) }
- let!(:async_index_destruction) { create(:postgres_async_index, :with_drop) }
+ let_it_be(:async_index_creation) { create(:postgres_async_index) }
+ let_it_be(:async_index_destruction) { create(:postgres_async_index, :with_drop) }
describe '.to_create' do
subject { described_class.to_create }
@@ -32,5 +34,33 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model
it { is_expected.to contain_exactly(async_index_destruction) }
end
+
+ describe '.ordered' do
+ before do
+ async_index_creation.update!(attempts: 3)
+ end
+
+ subject { described_class.ordered.limit(1) }
+
+ it { is_expected.to contain_exactly(async_index_destruction) }
+ end
+ end
+
+ describe '#handle_exception!' do
+ let_it_be_with_reload(:async_index_creation) { create(:postgres_async_index) }
+
+ let(:error) { instance_double(StandardError, message: 'Oups', backtrace: %w[this that]) }
+
+ subject { async_index_creation.handle_exception!(error) }
+
+ it 'increases the attempts number' do
+ expect { subject }.to change { async_index_creation.reload.attempts }.by(1)
+ end
+
+ it 'saves error details' do
+ subject
+
+ expect(async_index_creation.reload.last_error).to eq("Oups\nthis\nthat")
+ end
end
end
diff --git a/spec/lib/gitlab/database/async_indexes_spec.rb b/spec/lib/gitlab/database/async_indexes_spec.rb
index 8a5509f892f..c6991bf4e06 100644
--- a/spec/lib/gitlab/database/async_indexes_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::AsyncIndexes do
+RSpec.describe Gitlab::Database::AsyncIndexes, feature_category: :database do
describe '.create_pending_indexes!' do
subject { described_class.create_pending_indexes! }
@@ -11,9 +11,9 @@ RSpec.describe Gitlab::Database::AsyncIndexes do
end
it 'takes 2 pending indexes and creates those' do
- Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.to_create.order(:id).limit(2).each do |index|
- creator = double('index creator')
- expect(Gitlab::Database::AsyncIndexes::IndexCreator).to receive(:new).with(index).and_return(creator)
+ indexes = described_class::PostgresAsyncIndex.to_create.order(:id).limit(2).to_a
+
+ expect_next_instances_of(described_class::IndexCreator, 2, indexes) do |creator|
expect(creator).to receive(:perform)
end
@@ -29,13 +29,56 @@ RSpec.describe Gitlab::Database::AsyncIndexes do
end
it 'takes 2 pending indexes and destroys those' do
- Gitlab::Database::AsyncIndexes::PostgresAsyncIndex.to_drop.order(:id).limit(2).each do |index|
- destructor = double('index destructor')
- expect(Gitlab::Database::AsyncIndexes::IndexDestructor).to receive(:new).with(index).and_return(destructor)
+ indexes = described_class::PostgresAsyncIndex.to_drop.order(:id).limit(2).to_a
+
+ expect_next_instances_of(described_class::IndexDestructor, 2, indexes) do |destructor|
expect(destructor).to receive(:perform)
end
subject
end
end
+
+ describe '.execute_pending_actions!' do
+ subject { described_class.execute_pending_actions!(how_many: how_many) }
+
+ let_it_be(:failed_creation_entry) { create(:postgres_async_index, attempts: 5) }
+ let_it_be(:failed_removal_entry) { create(:postgres_async_index, :with_drop, attempts: 1) }
+ let_it_be(:creation_entry) { create(:postgres_async_index) }
+ let_it_be(:removal_entry) { create(:postgres_async_index, :with_drop) }
+
+ context 'with one entry' do
+ let(:how_many) { 1 }
+
+ it 'executes instructions ordered by attempts and ids' do
+ expect { subject }
+ .to change { queued_entries_exist?(creation_entry) }.to(false)
+ .and change { described_class::PostgresAsyncIndex.count }.by(-how_many)
+ end
+ end
+
+ context 'with two entries' do
+ let(:how_many) { 2 }
+
+ it 'executes instructions ordered by attempts' do
+ expect { subject }
+ .to change { queued_entries_exist?(creation_entry, removal_entry) }.to(false)
+ .and change { described_class::PostgresAsyncIndex.count }.by(-how_many)
+ end
+ end
+
+ context 'when the budget allows more instructions' do
+ let(:how_many) { 3 }
+
+ it 'retries failed attempts' do
+ expect { subject }
+ .to change { queued_entries_exist?(creation_entry, removal_entry, failed_removal_entry) }.to(false)
+ .and change { described_class::PostgresAsyncIndex.count }.by(-how_many)
+ end
+ end
+
+ def queued_entries_exist?(*records)
+ described_class::PostgresAsyncIndex.where(id: records).exists?
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 31ae5e9b55d..d132559acea 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -897,7 +897,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
it 'doesn not filter by gitlab schemas available for the connection if the column is nor present' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect(described_class).to receive(:gitlab_schema_column_exists?).and_return(false)
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index 852cc719d01..53f8fe3dcd2 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -58,10 +58,10 @@ RSpec.describe Gitlab::Database::BatchCount do
it 'reduces batch size by half and retry fetch' do
too_big_batch_relation_mock = instance_double(ActiveRecord::Relation)
allow(model).to receive_message_chain(:select, public_send: relation)
- allow(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size)).and_return(too_big_batch_relation_mock)
+ allow(relation).to receive(:where).with({ "id" => 0..calculate_batch_size(batch_size) }).and_return(too_big_batch_relation_mock)
allow(too_big_batch_relation_mock).to receive(:send).and_raise(ActiveRecord::QueryCanceled)
- expect(relation).to receive(:where).with("id" => 0..calculate_batch_size(batch_size / 2)).and_return(double(send: 1))
+ expect(relation).to receive(:where).with({ "id" => 0..calculate_batch_size(batch_size / 2) }).and_return(double(send: 1))
subject.call(model, column, batch_size: batch_size, start: 0)
end
@@ -146,7 +146,7 @@ RSpec.describe Gitlab::Database::BatchCount do
allow(model).to receive_message_chain(:select, public_send: relation)
batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE)
- expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
+ expect(relation).to receive(:where).with({ "id" => min_id..batch_end_id }).and_return(double(send: 1))
described_class.batch_count(model)
end
@@ -382,7 +382,7 @@ RSpec.describe Gitlab::Database::BatchCount do
allow(model).to receive_message_chain(:select, public_send: relation)
batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_DISTINCT_BATCH_SIZE)
- expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
+ expect(relation).to receive(:where).with({ "id" => min_id..batch_end_id }).and_return(double(send: 1))
described_class.batch_distinct_count(model)
end
@@ -468,7 +468,7 @@ RSpec.describe Gitlab::Database::BatchCount do
allow(model).to receive_message_chain(:select, public_send: relation)
batch_end_id = min_id + calculate_batch_size(Gitlab::Database::BatchCounter::DEFAULT_SUM_BATCH_SIZE)
- expect(relation).to receive(:where).with("id" => min_id..batch_end_id).and_return(double(send: 1))
+ expect(relation).to receive(:where).with({ "id" => min_id..batch_end_id }).and_return(double(send: 1))
described_class.batch_sum(model, column)
end
diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
index 1e316c55786..ff31a5cd6cb 100644
--- a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
@@ -11,304 +11,319 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
Gitlab::Database::LoadBalancing::Session.clear_session
end
- describe '#stick_or_unstick_request' do
- it 'sticks or unsticks a single object and updates the Rack environment' do
- expect(sticking)
- .to receive(:unstick_or_continue_sticking)
- .with(:user, 42)
-
- env = {}
-
- sticking.stick_or_unstick_request(env, :user, 42)
-
- expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a)
- .to eq([[sticking, :user, 42]])
+ shared_examples 'sticking' do
+ before do
+ allow(ActiveRecord::Base.load_balancer)
+ .to receive(:primary_write_location)
+ .and_return('foo')
end
- it 'sticks or unsticks multiple objects and updates the Rack environment' do
- expect(sticking)
- .to receive(:unstick_or_continue_sticking)
- .with(:user, 42)
- .ordered
+ it 'sticks an entity to the primary', :aggregate_failures do
+ allow(ActiveRecord::Base.load_balancer)
+ .to receive(:primary_only?)
+ .and_return(false)
- expect(sticking)
- .to receive(:unstick_or_continue_sticking)
- .with(:runner, '123456789')
- .ordered
+ ids.each do |id|
+ expect(sticking)
+ .to receive(:set_write_location_for)
+ .with(:user, id, 'foo')
+ end
- env = {}
+ expect(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:use_primary!)
- sticking.stick_or_unstick_request(env, :user, 42)
- sticking.stick_or_unstick_request(env, :runner, '123456789')
+ subject
+ end
- expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a).to eq(
- [
- [sticking, :user, 42],
- [sticking, :runner,
- '123456789']
- ])
+ it 'does not update the write location when no replicas are used' do
+ expect(sticking).not_to receive(:set_write_location_for)
+
+ subject
end
end
- describe '#stick_if_necessary' do
- it 'does not stick if no write was performed' do
- allow(Gitlab::Database::LoadBalancing::Session.current)
- .to receive(:performed_write?)
- .and_return(false)
+ shared_examples 'tracking status in redis' do
+ describe '#stick_or_unstick_request' do
+ it 'sticks or unsticks a single object and updates the Rack environment' do
+ expect(sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
- expect(sticking).not_to receive(:stick)
+ env = {}
- sticking.stick_if_necessary(:user, 42)
- end
+ sticking.stick_or_unstick_request(env, :user, 42)
- it 'sticks to the primary if a write was performed' do
- allow(Gitlab::Database::LoadBalancing::Session.current)
- .to receive(:performed_write?)
- .and_return(true)
+ expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a)
+ .to eq([[sticking, :user, 42]])
+ end
- expect(sticking)
- .to receive(:stick)
- .with(:user, 42)
+ it 'sticks or unsticks multiple objects and updates the Rack environment' do
+ expect(sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+ .ordered
- sticking.stick_if_necessary(:user, 42)
- end
- end
+ expect(sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:runner, '123456789')
+ .ordered
- describe '#all_caught_up?' do
- let(:lb) { ActiveRecord::Base.load_balancer }
- let(:last_write_location) { 'foo' }
+ env = {}
- before do
- allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
+ sticking.stick_or_unstick_request(env, :user, 42)
+ sticking.stick_or_unstick_request(env, :runner, '123456789')
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:user, 42)
- .and_return(last_write_location)
+ expect(env[Gitlab::Database::LoadBalancing::RackMiddleware::STICK_OBJECT].to_a).to eq(
+ [
+ [sticking, :user, 42],
+ [sticking, :runner,
+ '123456789']
+ ])
+ end
end
- context 'when no write location could be found' do
- let(:last_write_location) { nil }
+ describe '#stick_if_necessary' do
+ it 'does not stick if no write was performed' do
+ allow(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:performed_write?)
+ .and_return(false)
- it 'returns true' do
- expect(lb).not_to receive(:select_up_to_date_host)
+ expect(sticking).not_to receive(:stick)
- expect(sticking.all_caught_up?(:user, 42)).to eq(true)
+ sticking.stick_if_necessary(:user, 42)
end
- end
- context 'when all secondaries have caught up' do
- before do
- allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(true)
- end
+ it 'sticks to the primary if a write was performed' do
+ allow(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:performed_write?)
+ .and_return(true)
- it 'returns true, and unsticks' do
expect(sticking)
- .to receive(:unstick)
+ .to receive(:stick)
.with(:user, 42)
- expect(sticking.all_caught_up?(:user, 42)).to eq(true)
- end
-
- it 'notifies with the proper event payload' do
- expect(ActiveSupport::Notifications)
- .to receive(:instrument)
- .with('caught_up_replica_pick.load_balancing', { result: true })
- .and_call_original
-
- sticking.all_caught_up?(:user, 42)
+ sticking.stick_if_necessary(:user, 42)
end
end
- context 'when the secondaries have not yet caught up' do
+ describe '#all_caught_up?' do
+ let(:lb) { ActiveRecord::Base.load_balancer }
+ let(:last_write_location) { 'foo' }
+
before do
- allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(false)
- end
+ allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
- it 'returns false' do
- expect(sticking.all_caught_up?(:user, 42)).to eq(false)
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return(last_write_location)
end
- it 'notifies with the proper event payload' do
- expect(ActiveSupport::Notifications)
- .to receive(:instrument)
- .with('caught_up_replica_pick.load_balancing', { result: false })
- .and_call_original
+ context 'when no write location could be found' do
+ let(:last_write_location) { nil }
+
+ it 'returns true' do
+ expect(lb).not_to receive(:select_up_to_date_host)
- sticking.all_caught_up?(:user, 42)
+ expect(sticking.all_caught_up?(:user, 42)).to eq(true)
+ end
end
- end
- end
- describe '#unstick_or_continue_sticking' do
- let(:lb) { ActiveRecord::Base.load_balancer }
+ context 'when all secondaries have caught up' do
+ before do
+ allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(true)
+ end
- it 'simply returns if no write location could be found' do
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:user, 42)
- .and_return(nil)
+ it 'returns true, and unsticks' do
+ expect(sticking)
+ .to receive(:unstick)
+ .with(:user, 42)
- expect(lb).not_to receive(:select_up_to_date_host)
+ expect(sticking.all_caught_up?(:user, 42)).to eq(true)
+ end
- sticking.unstick_or_continue_sticking(:user, 42)
- end
+ it 'notifies with the proper event payload' do
+ expect(ActiveSupport::Notifications)
+ .to receive(:instrument)
+ .with('caught_up_replica_pick.load_balancing', { result: true })
+ .and_call_original
- it 'unsticks if all secondaries have caught up' do
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:user, 42)
- .and_return('foo')
+ sticking.all_caught_up?(:user, 42)
+ end
+ end
- allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(true)
+ context 'when the secondaries have not yet caught up' do
+ before do
+ allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(false)
+ end
- expect(sticking)
- .to receive(:unstick)
- .with(:user, 42)
+ it 'returns false' do
+ expect(sticking.all_caught_up?(:user, 42)).to eq(false)
+ end
- sticking.unstick_or_continue_sticking(:user, 42)
+ it 'notifies with the proper event payload' do
+ expect(ActiveSupport::Notifications)
+ .to receive(:instrument)
+ .with('caught_up_replica_pick.load_balancing', { result: false })
+ .and_call_original
+
+ sticking.all_caught_up?(:user, 42)
+ end
+ end
end
- it 'continues using the primary if the secondaries have not yet caught up' do
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:user, 42)
- .and_return('foo')
+ describe '#unstick_or_continue_sticking' do
+ let(:lb) { ActiveRecord::Base.load_balancer }
- allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(false)
+ it 'simply returns if no write location could be found' do
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return(nil)
- expect(Gitlab::Database::LoadBalancing::Session.current)
- .to receive(:use_primary!)
+ expect(lb).not_to receive(:select_up_to_date_host)
- sticking.unstick_or_continue_sticking(:user, 42)
- end
- end
+ sticking.unstick_or_continue_sticking(:user, 42)
+ end
- RSpec.shared_examples 'sticking' do
- before do
- allow(ActiveRecord::Base.load_balancer)
- .to receive(:primary_write_location)
- .and_return('foo')
- end
+ it 'unsticks if all secondaries have caught up' do
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
- it 'sticks an entity to the primary', :aggregate_failures do
- allow(ActiveRecord::Base.load_balancer)
- .to receive(:primary_only?)
- .and_return(false)
+ allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(true)
- ids.each do |id|
expect(sticking)
- .to receive(:set_write_location_for)
- .with(:user, id, 'foo')
+ .to receive(:unstick)
+ .with(:user, 42)
+
+ sticking.unstick_or_continue_sticking(:user, 42)
end
- expect(Gitlab::Database::LoadBalancing::Session.current)
- .to receive(:use_primary!)
+ it 'continues using the primary if the secondaries have not yet caught up' do
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
- subject
- end
+ allow(lb).to receive(:select_up_to_date_host).with('foo').and_return(false)
- it 'does not update the write location when no replicas are used' do
- expect(sticking).not_to receive(:set_write_location_for)
+ expect(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:use_primary!)
- subject
+ sticking.unstick_or_continue_sticking(:user, 42)
+ end
end
- end
- describe '#stick' do
- it_behaves_like 'sticking' do
- let(:ids) { [42] }
- subject { sticking.stick(:user, ids.first) }
+ describe '#stick' do
+ it_behaves_like 'sticking' do
+ let(:ids) { [42] }
+ subject { sticking.stick(:user, ids.first) }
+ end
end
- end
- describe '#bulk_stick' do
- it_behaves_like 'sticking' do
- let(:ids) { [42, 43] }
- subject { sticking.bulk_stick(:user, ids) }
+ describe '#bulk_stick' do
+ it_behaves_like 'sticking' do
+ let(:ids) { [42, 43] }
+ subject { sticking.bulk_stick(:user, ids) }
+ end
end
- end
- describe '#mark_primary_write_location' do
- it 'updates the write location with the load balancer' do
- allow(ActiveRecord::Base.load_balancer)
- .to receive(:primary_write_location)
- .and_return('foo')
+ describe '#mark_primary_write_location' do
+ it 'updates the write location with the load balancer' do
+ allow(ActiveRecord::Base.load_balancer)
+ .to receive(:primary_write_location)
+ .and_return('foo')
- allow(ActiveRecord::Base.load_balancer)
- .to receive(:primary_only?)
- .and_return(false)
+ allow(ActiveRecord::Base.load_balancer)
+ .to receive(:primary_only?)
+ .and_return(false)
+
+ expect(sticking)
+ .to receive(:set_write_location_for)
+ .with(:user, 42, 'foo')
+
+ sticking.mark_primary_write_location(:user, 42)
+ end
- expect(sticking)
- .to receive(:set_write_location_for)
- .with(:user, 42, 'foo')
+ it 'does nothing when no replicas are used' do
+ expect(sticking).not_to receive(:set_write_location_for)
- sticking.mark_primary_write_location(:user, 42)
+ sticking.mark_primary_write_location(:user, 42)
+ end
end
- it 'does nothing when no replicas are used' do
- expect(sticking).not_to receive(:set_write_location_for)
+ describe '#unstick' do
+ it 'removes the sticking data from Redis' do
+ sticking.set_write_location_for(:user, 4, 'foo')
+ sticking.unstick(:user, 4)
- sticking.mark_primary_write_location(:user, 42)
+ expect(sticking.last_write_location_for(:user, 4)).to be_nil
+ end
end
- end
- describe '#unstick' do
- it 'removes the sticking data from Redis' do
- sticking.set_write_location_for(:user, 4, 'foo')
- sticking.unstick(:user, 4)
+ describe '#last_write_location_for' do
+ it 'returns the last WAL write location for a user' do
+ sticking.set_write_location_for(:user, 4, 'foo')
- expect(sticking.last_write_location_for(:user, 4)).to be_nil
+ expect(sticking.last_write_location_for(:user, 4)).to eq('foo')
+ end
end
- end
- describe '#last_write_location_for' do
- it 'returns the last WAL write location for a user' do
- sticking.set_write_location_for(:user, 4, 'foo')
+ describe '#select_caught_up_replicas' do
+ let(:lb) { ActiveRecord::Base.load_balancer }
+
+ context 'with no write location' do
+ before do
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:project, 42)
+ .and_return(nil)
+ end
+
+ it 'returns false and does not try to find caught up hosts' do
+ expect(lb).not_to receive(:select_up_to_date_host)
+ expect(sticking.select_caught_up_replicas(:project, 42)).to be false
+ end
+ end
- expect(sticking.last_write_location_for(:user, 4)).to eq('foo')
+ context 'with write location' do
+ before do
+ allow(sticking)
+ .to receive(:last_write_location_for)
+ .with(:project, 42)
+ .and_return('foo')
+ end
+
+ it 'returns true, selects hosts, and unsticks if any secondary has caught up' do
+ expect(lb).to receive(:select_up_to_date_host).and_return(true)
+ expect(sticking)
+ .to receive(:unstick)
+ .with(:project, 42)
+ expect(sticking.select_caught_up_replicas(:project, 42)).to be true
+ end
+ end
end
end
- describe '#redis_key_for' do
- it 'returns a String' do
- expect(sticking.redis_key_for(:user, 42))
- .to eq('database-load-balancing/write-location/main/user/42')
- end
+ context 'with multi-store feature flags turned on' do
+ it_behaves_like 'tracking status in redis'
end
- describe '#select_caught_up_replicas' do
- let(:lb) { ActiveRecord::Base.load_balancer }
-
- context 'with no write location' do
- before do
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:project, 42)
- .and_return(nil)
- end
-
- it 'returns false and does not try to find caught up hosts' do
- expect(lb).not_to receive(:select_up_to_date_host)
- expect(sticking.select_caught_up_replicas(:project, 42)).to be false
- end
+ context 'when both multi-store feature flags are off' do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores_for_db_load_balancing: false)
+ stub_feature_flags(use_primary_store_as_default_for_db_load_balancing: false)
end
- context 'with write location' do
- before do
- allow(sticking)
- .to receive(:last_write_location_for)
- .with(:project, 42)
- .and_return('foo')
- end
+ it_behaves_like 'tracking status in redis'
+ end
- it 'returns true, selects hosts, and unsticks if any secondary has caught up' do
- expect(lb).to receive(:select_up_to_date_host).and_return(true)
- expect(sticking)
- .to receive(:unstick)
- .with(:project, 42)
- expect(sticking.select_caught_up_replicas(:project, 42)).to be true
- end
+ describe '#redis_key_for' do
+ it 'returns a String' do
+ expect(sticking.redis_key_for(:user, 42))
+ .to eq('database-load-balancing/write-location/main/user/42')
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
index 1eb077fe6ca..56fbaef031d 100644
--- a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis, :delete do
+RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis, :delete, feature_category: :database do # rubocop:disable Layout/LineLength
include StubENV
let(:model) { ActiveRecord::Base }
let(:db_host) { model.connection_pool.db_config.host }
@@ -55,50 +55,8 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
conn.execute("INSERT INTO #{test_table_name} (value) VALUES (2)")
end
- context 'with the PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION environment variable not set' do
- it 'logs a warning when violating transaction semantics with writes' do
- conn = model.connection
-
- expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :transaction_leak))
- expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :read_write_retry))
-
- conn.transaction do
- expect(conn).to be_transaction_open
-
- execute(conn)
-
- expect(conn).not_to be_transaction_open
- end
-
- values = conn.execute("SELECT value FROM #{test_table_name}").to_a.map { |row| row['value'] }
- expect(values).to contain_exactly(2) # Does not include 1 because the transaction was aborted and leaked
- end
-
- it 'does not log a warning when no transaction is open to be leaked' do
- conn = model.connection
-
- expect(::Gitlab::Database::LoadBalancing::Logger)
- .not_to receive(:warn).with(hash_including(event: :transaction_leak))
- expect(::Gitlab::Database::LoadBalancing::Logger)
- .to receive(:warn).with(hash_including(event: :read_write_retry))
-
- expect(conn).not_to be_transaction_open
-
- execute(conn)
-
- expect(conn).not_to be_transaction_open
-
- values = conn.execute("SELECT value FROM #{test_table_name}").to_a.map { |row| row['value'] }
- expect(values).to contain_exactly(1, 2) # Includes both rows because there was no transaction to roll back
- end
- end
-
- context 'with the PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION environment variable set' do
- before do
- stub_env('PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION' => '1')
- end
-
- it 'raises an exception when a retry would occur during a transaction' do
+ context 'in a transaction' do
+ it 'raises an exception when a retry would occur' do
expect(::Gitlab::Database::LoadBalancing::Logger)
.not_to receive(:warn).with(hash_including(event: :transaction_leak))
@@ -108,8 +66,10 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
end
end.to raise_error(ActiveRecord::StatementInvalid) { |e| expect(e.cause).to be_a(PG::ConnectionBad) }
end
+ end
- it 'retries when not in a transaction' do
+ context 'without a transaction' do
+ it 'retries' do
expect(::Gitlab::Database::LoadBalancing::Logger)
.not_to receive(:warn).with(hash_including(event: :transaction_leak))
expect(::Gitlab::Database::LoadBalancing::Logger)
diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb
index 1c85abac91c..59e16e6ca8b 100644
--- a/spec/lib/gitlab/database/load_balancing_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::LoadBalancing, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe Gitlab::Database::LoadBalancing, :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
describe '.base_models' do
it 'returns the models to apply load balancing to' do
models = described_class.base_models
diff --git a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
index 089c7a779f2..be9346e3829 100644
--- a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
let(:create_gitlab_shared_table_migration_class) { create_table_migration(gitlab_shared_table_name) }
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'does not lock any newly created tables' do
@@ -106,7 +106,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
let(:migration_class) { create_table_migration(table_name, skip_automatic_lock_on_writes) }
@@ -238,7 +238,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when renaming a table' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
create_table_migration(old_table_name).migrate(:up) # create the table first before renaming it
end
@@ -277,7 +277,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
let(:config_model) { Gitlab::Database.database_base_models[:main] }
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'does not lock any newly created tables' do
@@ -305,7 +305,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
migration_class.connection.execute("CREATE TABLE #{table_name}()")
migration_class.migrate(:up)
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 12fa115cc4e..9df23776be8 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1047,59 +1047,63 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
describe '#foreign_key_exists?' do
+ let(:referenced_table_name) { '_test_gitlab_main_referenced' }
+ let(:referencing_table_name) { '_test_gitlab_main_referencing' }
+
before do
model.connection.execute(<<~SQL)
- create table referenced (
+ create table #{referenced_table_name} (
id bigserial primary key not null
);
- create table referencing (
+ create table #{referencing_table_name} (
id bigserial primary key not null,
non_standard_id bigint not null,
- constraint fk_referenced foreign key (non_standard_id) references referenced(id) on delete cascade
+ constraint fk_referenced foreign key (non_standard_id)
+ references #{referenced_table_name}(id) on delete cascade
);
SQL
end
shared_examples_for 'foreign key checks' do
it 'finds existing foreign keys by column' do
- expect(model.foreign_key_exists?(:referencing, target_table, column: :non_standard_id)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, column: :non_standard_id)).to be_truthy
end
it 'finds existing foreign keys by name' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :fk_referenced)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :fk_referenced)).to be_truthy
end
it 'finds existing foreign_keys by name and column' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :fk_referenced, column: :non_standard_id)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :fk_referenced, column: :non_standard_id)).to be_truthy
end
it 'finds existing foreign_keys by name, column and on_delete' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :fk_referenced, column: :non_standard_id, on_delete: :cascade)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :fk_referenced, column: :non_standard_id, on_delete: :cascade)).to be_truthy
end
it 'finds existing foreign keys by target table only' do
- expect(model.foreign_key_exists?(:referencing, target_table)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table)).to be_truthy
end
it 'compares by column name if given' do
- expect(model.foreign_key_exists?(:referencing, target_table, column: :user_id)).to be_falsey
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, column: :user_id)).to be_falsey
end
it 'compares by target column name if given' do
- expect(model.foreign_key_exists?(:referencing, target_table, primary_key: :user_id)).to be_falsey
- expect(model.foreign_key_exists?(:referencing, target_table, primary_key: :id)).to be_truthy
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, primary_key: :user_id)).to be_falsey
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, primary_key: :id)).to be_truthy
end
it 'compares by foreign key name if given' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :non_existent_foreign_key_name)).to be_falsey
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :non_existent_foreign_key_name)).to be_falsey
end
it 'compares by foreign key name and column if given' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :non_existent_foreign_key_name, column: :non_standard_id)).to be_falsey
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :non_existent_foreign_key_name, column: :non_standard_id)).to be_falsey
end
it 'compares by foreign key name, column and on_delete if given' do
- expect(model.foreign_key_exists?(:referencing, target_table, name: :fk_referenced, column: :non_standard_id, on_delete: :nullify)).to be_falsey
+ expect(model.foreign_key_exists?(referencing_table_name, target_table, name: :fk_referenced, column: :non_standard_id, on_delete: :nullify)).to be_falsey
end
end
@@ -1110,7 +1114,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
context 'specifying a target table' do
- let(:target_table) { :referenced }
+ let(:target_table) { referenced_table_name }
it_behaves_like 'foreign key checks'
end
@@ -1121,64 +1125,78 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it 'raises an error if an invalid on_delete is specified' do
# The correct on_delete key is "nullify"
- expect { model.foreign_key_exists?(:referenced, on_delete: :set_null) }.to raise_error(ArgumentError)
+ expect { model.foreign_key_exists?(referenced_table_name, on_delete: :set_null) }.to raise_error(ArgumentError)
end
context 'with foreign key using multiple columns' do
+ let(:p_referenced_table_name) { '_test_gitlab_main_p_referenced' }
+ let(:p_referencing_table_name) { '_test_gitlab_main_p_referencing' }
+
before do
model.connection.execute(<<~SQL)
- create table p_referenced (
- id bigserial not null,
- partition_number bigint not null default 100,
- primary key (partition_number, id)
- );
- create table p_referencing (
- id bigserial primary key not null,
- partition_number bigint not null,
- constraint fk_partitioning foreign key (partition_number, id) references p_referenced(partition_number, id) on delete cascade
- );
+ create table #{p_referenced_table_name} (
+ id bigserial not null,
+ partition_number bigint not null default 100,
+ primary key (partition_number, id)
+ );
+ create table #{p_referencing_table_name} (
+ id bigserial primary key not null,
+ partition_number bigint not null,
+ constraint fk_partitioning foreign key (partition_number, id)
+ references #{p_referenced_table_name} (partition_number, id) on delete cascade
+ );
SQL
end
it 'finds existing foreign keys by columns' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, column: [:partition_number, :id])).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ column: [:partition_number, :id])).to be_truthy
end
it 'finds existing foreign keys by name' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :fk_partitioning)).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :fk_partitioning)).to be_truthy
end
it 'finds existing foreign_keys by name and column' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :fk_partitioning, column: [:partition_number, :id])).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :fk_partitioning, column: [:partition_number, :id])).to be_truthy
end
it 'finds existing foreign_keys by name, column and on_delete' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :fk_partitioning, column: [:partition_number, :id], on_delete: :cascade)).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :fk_partitioning, column: [:partition_number, :id], on_delete: :cascade)).to be_truthy
end
it 'finds existing foreign keys by target table only' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced)).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name)).to be_truthy
end
it 'compares by column name if given' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, column: :id)).to be_falsey
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ column: :id)).to be_falsey
end
it 'compares by target column name if given' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, primary_key: :user_id)).to be_falsey
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, primary_key: [:partition_number, :id])).to be_truthy
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ primary_key: :user_id)).to be_falsey
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ primary_key: [:partition_number, :id])).to be_truthy
end
it 'compares by foreign key name if given' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :non_existent_foreign_key_name)).to be_falsey
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :non_existent_foreign_key_name)).to be_falsey
end
it 'compares by foreign key name and column if given' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :non_existent_foreign_key_name, column: [:partition_number, :id])).to be_falsey
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :non_existent_foreign_key_name, column: [:partition_number, :id])).to be_falsey
end
it 'compares by foreign key name, column and on_delete if given' do
- expect(model.foreign_key_exists?(:p_referencing, :p_referenced, name: :fk_partitioning, column: [:partition_number, :id], on_delete: :nullify)).to be_falsey
+ expect(model.foreign_key_exists?(p_referencing_table_name, p_referenced_table_name,
+ name: :fk_partitioning, column: [:partition_number, :id], on_delete: :nullify)).to be_falsey
end
end
end
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
index 97b432406eb..1365adb8993 100644
--- a/spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_migration_last_id_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Migrations::BatchedMigrationLastId, feature_category: :pipeline_insights do
+RSpec.describe Gitlab::Database::Migrations::BatchedMigrationLastId, feature_category: :database do
subject(:test_sampling) { described_class.new(connection, base_dir) }
let(:base_dir) { Pathname.new(Dir.mktmpdir) }
diff --git a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
index b0bdbf5c371..4f347034c0b 100644
--- a/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+++ b/spec/lib/gitlab/database/migrations/instrumentation_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
let(:migration_name) { 'test' }
let(:migration_version) { '12345' }
let(:migration_meta) { { 'max_batch_size' => 1, 'total_tuple_count' => 10, 'interval' => 60 } }
+ let(:expected_json_keys) { %w[version name walltime success total_database_size_change query_statistics] }
it 'executes the given block' do
expect do |b|
@@ -81,7 +82,10 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
expect(subject.success).to be_truthy
expect(subject.version).to eq(migration_version)
expect(subject.name).to eq(migration_name)
- expect(subject.meta).to eq(migration_meta)
+ end
+
+ it 'transforms observation to expected json' do
+ expect(Gitlab::Json.parse(subject.to_json).keys).to contain_exactly(*expected_json_keys)
end
end
@@ -114,7 +118,10 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
expect(subject['success']).to be_falsey
expect(subject['version']).to eq(migration_version)
expect(subject['name']).to eq(migration_name)
- expect(subject['meta']).to include(migration_meta)
+ end
+
+ it 'transforms observation to expected json' do
+ expect(Gitlab::Json.parse(subject.to_json).keys).to contain_exactly(*expected_json_keys)
end
end
end
diff --git a/spec/lib/gitlab/database/migrations/observers/batch_details_spec.rb b/spec/lib/gitlab/database/migrations/observers/batch_details_spec.rb
new file mode 100644
index 00000000000..5b3c23736ed
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/observers/batch_details_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::Observers::BatchDetails, feature_category: :database do
+ subject(:observe) { described_class.new(observation, path, connection) }
+
+ let(:connection) { ActiveRecord::Migration.connection }
+ let(:observation) { Gitlab::Database::Migrations::Observation.new(meta: meta) }
+ let(:path) { Dir.mktmpdir }
+ let(:file_name) { 'batch-details.json' }
+ let(:file_path) { Pathname.new(path).join(file_name) }
+ let(:json_file) { Gitlab::Json.parse(File.read(file_path)) }
+ let(:job_meta) do
+ { "min_value" => 1, "max_value" => 19, "batch_size" => 20, "sub_batch_size" => 5, "pause_ms" => 100 }
+ end
+
+ where(:meta, :expected_keys) do
+ [
+ [lazy { { job_meta: job_meta } }, %w[time_spent min_value max_value batch_size sub_batch_size pause_ms]],
+ [nil, %w[time_spent]],
+ [{ job_meta: nil }, %w[time_spent]]
+ ]
+ end
+
+ with_them do
+ before do
+ observe.before
+ observe.after
+ end
+
+ after do
+ FileUtils.remove_entry(path)
+ end
+
+ it 'records expected information to file' do
+ observe.record
+
+ expect(json_file.keys).to match_array(expected_keys)
+ 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 0b048617ce1..57c5011590c 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freeze_time do
+RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freeze_time, feature_category: :database do
include Gitlab::Database::MigrationHelpers
include Database::MigrationTestingHelpers
@@ -45,18 +45,15 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
end
with_them do
- let(:result_dir) { Dir.mktmpdir }
+ let(:result_dir) { Pathname.new(Dir.mktmpdir) }
+ let(:connection) { base_model.connection }
+ let(:table_name) { "_test_column_copying" }
+ let(:from_id) { 0 }
after do
FileUtils.rm_rf(result_dir)
end
- let(:connection) { base_model.connection }
-
- let(:table_name) { "_test_column_copying" }
-
- let(:from_id) { 0 }
-
before do
connection.execute(<<~SQL)
CREATE TABLE #{table_name} (
@@ -70,26 +67,15 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
context 'running a real background migration' do
let(:interval) { 5.minutes }
- let(:meta) { { "max_batch_size" => nil, "total_tuple_count" => nil, "interval" => interval } }
-
- let(:params) do
- {
- version: nil,
- connection: connection,
- meta: {
- interval: 300,
- max_batch_size: nil,
- total_tuple_count: nil
- }
- }
- end
+ let(:params) { { version: nil, connection: connection } }
+ let(:migration_name) { 'CopyColumnUsingBackgroundMigrationJob' }
+ let(:migration_file_path) { result_dir.join('CopyColumnUsingBackgroundMigrationJob', 'details.json') }
+ let(:json_file) { Gitlab::Json.parse(File.read(migration_file_path)) }
+ let(:expected_file_keys) { %w[interval total_tuple_count max_batch_size] }
before do
- queue_migration('CopyColumnUsingBackgroundMigrationJob',
- table_name, :id,
- :id, :data,
- batch_size: 100,
- job_interval: interval) # job_interval is skipped when testing
+ # job_interval is skipped when testing
+ queue_migration(migration_name, table_name, :id, :id, :data, batch_size: 100, job_interval: interval)
end
subject(:sample_migration) do
@@ -113,6 +99,20 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
subject
end
+
+ it 'uses the filtering clause from the migration' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |s|
+ expect(s).to receive(:filter_batch).at_least(:once).and_call_original
+ end
+
+ subject
+ end
+
+ it 'exports migration details to a file' do
+ subject
+
+ expect(json_file.keys).to match_array(expected_file_keys)
+ end
end
context 'with jobs to run' do
diff --git a/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb b/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb
index d35211af680..ee63ea7174b 100644
--- a/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Migrations::TimeoutHelpers do
+RSpec.describe Gitlab::Database::Migrations::TimeoutHelpers, feature_category: :database do
let(:model) do
ActiveRecord::Migration.new.extend(described_class)
end
diff --git a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
index 8027990a546..2212cb09888 100644
--- a/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/partition_manager_spec.rb
@@ -6,22 +6,14 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
include Database::PartitioningHelpers
include ExclusiveLeaseHelpers
- def has_partition(model, month)
- Gitlab::Database::PostgresPartition.for_parent_table(model.table_name).any? do |partition|
- Gitlab::Database::Partitioning::TimePartition.from_sql(
- model.table_name,
- partition.name,
- partition.condition
- ).from == month
- end
- end
+ let(:partitioned_table_name) { "_test_gitlab_main_my_model_example_table" }
context 'creating partitions (mocked)' do
subject(:sync_partitions) { described_class.new(model).sync_partitions }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:connection) { ActiveRecord::Base.connection }
- let(:table) { "my_model_example_table" }
+ let(:table) { partitioned_table_name }
let(:partitioning_strategy) do
double(missing_partitions: partitions, extra_partitions: [], after_adding_partitions: nil)
end
@@ -57,7 +49,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
let(:connection) { Ci::ApplicationRecord.connection }
it 'uses the explicitly provided connection when any' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect(connection).to receive(:execute).with("LOCK TABLE \"#{table}\" IN ACCESS EXCLUSIVE MODE")
expect(connection).to receive(:execute).with(partitions.first.to_sql)
@@ -102,14 +94,14 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
Class.new(ApplicationRecord) do
include PartitionedTable
- self.table_name = 'my_model_example_table'
-
partitioned_by :created_at, strategy: :monthly
end
end
before do
- create_partitioned_table(connection, 'my_model_example_table')
+ my_model.table_name = partitioned_table_name
+
+ create_partitioned_table(connection, partitioned_table_name)
end
it 'creates partitions' do
@@ -184,27 +176,27 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
Class.new(ApplicationRecord) do
include PartitionedTable
- self.table_name = 'my_model_example_table'
-
partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
end
end
before do
connection.execute(<<~SQL)
- CREATE TABLE my_model_example_table
+ CREATE TABLE #{partitioned_table_name}
(id serial not null, created_at timestamptz not null, primary key (id, created_at))
PARTITION BY RANGE (created_at);
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.my_model_example_table_202104
- PARTITION OF my_model_example_table
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partitioned_table_name}_202104
+ PARTITION OF #{partitioned_table_name}
FOR VALUES FROM ('2021-04-01') TO ('2021-05-01');
- CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.my_model_example_table_202105
- PARTITION OF my_model_example_table
+ CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{partitioned_table_name}_202105
+ PARTITION OF #{partitioned_table_name}
FOR VALUES FROM ('2021-05-01') TO ('2021-06-01');
SQL
+ my_model.table_name = partitioned_table_name
+
# Also create all future partitions so that the sync is only trying to detach old partitions
my_model.partitioning_strategy.missing_partitions.each do |p|
connection.execute p.to_sql
@@ -234,7 +226,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
it 'creates the appropriate PendingPartitionDrop entry' do
subject
- pending_drop = Postgresql::DetachedPartition.find_by!(table_name: 'my_model_example_table_202104')
+ pending_drop = Postgresql::DetachedPartition.find_by!(table_name: "#{partitioned_table_name}_202104")
expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR)
end
@@ -243,11 +235,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
context 'when the model is the target of a foreign key' do
before do
connection.execute(<<~SQL)
- create unique index idx_for_fk ON my_model_example_table(created_at);
+ create unique index idx_for_fk ON #{partitioned_table_name}(created_at);
- create table referencing_table (
+ create table _test_gitlab_main_referencing_table (
id bigserial primary key not null,
- referencing_created_at timestamptz references my_model_example_table(created_at)
+ referencing_created_at timestamptz references #{partitioned_table_name}(created_at)
);
SQL
end
@@ -265,15 +257,15 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
Class.new(ApplicationRecord) do
include PartitionedTable
- self.table_name = 'my_model_example_table'
-
partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
end
end
before do
+ my_model.table_name = partitioned_table_name
+
connection.execute(<<~SQL)
- CREATE TABLE my_model_example_table
+ CREATE TABLE #{partitioned_table_name}
(id serial not null, created_at timestamptz not null, primary key (id, created_at))
PARTITION BY RANGE (created_at);
SQL
@@ -294,6 +286,16 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
end
+ def has_partition(model, month)
+ Gitlab::Database::PostgresPartition.for_parent_table(model.table_name).any? do |partition|
+ Gitlab::Database::Partitioning::TimePartition.from_sql(
+ model.table_name,
+ partition.name,
+ partition.condition
+ ).from == month
+ end
+ end
+
def create_partitioned_table(connection, table)
connection.execute(<<~SQL)
CREATE TABLE #{table}
diff --git a/spec/lib/gitlab/database/partitioning_spec.rb b/spec/lib/gitlab/database/partitioning_spec.rb
index 855d0bc46a4..ae74ee60a4b 100644
--- a/spec/lib/gitlab/database/partitioning_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe Gitlab::Database::Partitioning do
end
it 'creates partitions in each database' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect { described_class.sync_partitions(models) }
.to change { find_partitions(table_names.first, conn: connection).size }.from(0)
@@ -176,7 +176,7 @@ RSpec.describe Gitlab::Database::Partitioning do
end
it 'manages partitions for models for the given database', :aggregate_failures do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect { described_class.sync_partitions([models.first, ci_model], only_on: 'ci') }
.to change { find_partitions(ci_model.table_name, conn: ci_connection).size }.from(0)
diff --git a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
index a8dbc4be16f..ae56f66737d 100644
--- a/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
+++ b/spec/lib/gitlab/database/postgres_foreign_key_spec.rb
@@ -6,26 +6,32 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
# PostgresForeignKey does not `behaves_like 'a postgres model'` because it does not correspond 1-1 with a single entry
# in pg_class
+ let(:table_prefix) { '_test_gitlab_main' }
+
before do
ApplicationRecord.connection.execute(<<~SQL)
- CREATE TABLE public.referenced_table (
+ CREATE TABLE #{schema_table_name('referenced_table')} (
id bigserial primary key not null,
id_b bigserial not null,
UNIQUE (id, id_b)
);
- CREATE TABLE public.other_referenced_table (
+ CREATE TABLE #{schema_table_name('other_referenced_table')} (
id bigserial primary key not null
);
- CREATE TABLE public.constrained_table (
+ CREATE TABLE #{schema_table_name('constrained_table')} (
id bigserial primary key not null,
referenced_table_id bigint not null,
referenced_table_id_b bigint not null,
other_referenced_table_id bigint not null,
- CONSTRAINT fk_constrained_to_referenced FOREIGN KEY(referenced_table_id, referenced_table_id_b) REFERENCES referenced_table(id, id_b) on delete restrict,
+ CONSTRAINT fk_constrained_to_referenced
+ FOREIGN KEY(referenced_table_id, referenced_table_id_b)
+ REFERENCES #{table_name('referenced_table')}(id, id_b)
+ ON DELETE restrict
+ ON UPDATE restrict,
CONSTRAINT fk_constrained_to_other_referenced FOREIGN KEY(other_referenced_table_id)
- REFERENCES other_referenced_table(id)
+ REFERENCES #{table_name('other_referenced_table')}(id)
);
SQL
@@ -33,13 +39,13 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
describe '#by_referenced_table_identifier' do
it 'throws an error when the identifier name is not fully qualified' do
- expect { described_class.by_referenced_table_identifier('referenced_table') }.to raise_error(ArgumentError, /not fully qualified/)
+ expect { described_class.by_referenced_table_identifier(table_name("referenced_table")) }.to raise_error(ArgumentError, /not fully qualified/)
end
it 'finds the foreign keys for the referenced table' do
expected = described_class.find_by!(name: 'fk_constrained_to_referenced')
- expect(described_class.by_referenced_table_identifier('public.referenced_table')).to contain_exactly(expected)
+ expect(described_class.by_referenced_table_identifier(schema_table_name("referenced_table"))).to contain_exactly(expected)
end
end
@@ -47,19 +53,19 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
it 'finds the foreign keys for the referenced table' do
expected = described_class.find_by!(name: 'fk_constrained_to_referenced')
- expect(described_class.by_referenced_table_name('referenced_table')).to contain_exactly(expected)
+ expect(described_class.by_referenced_table_name(table_name("referenced_table"))).to contain_exactly(expected)
end
end
describe '#by_constrained_table_identifier' do
it 'throws an error when the identifier name is not fully qualified' do
- expect { described_class.by_constrained_table_identifier('constrained_table') }.to raise_error(ArgumentError, /not fully qualified/)
+ expect { described_class.by_constrained_table_identifier(table_name("constrained_table")) }.to raise_error(ArgumentError, /not fully qualified/)
end
it 'finds the foreign keys for the constrained table' do
expected = described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a
- expect(described_class.by_constrained_table_identifier('public.constrained_table')).to match_array(expected)
+ expect(described_class.by_constrained_table_identifier(schema_table_name('constrained_table'))).to match_array(expected)
end
end
@@ -67,7 +73,7 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
it 'finds the foreign keys for the constrained table' do
expected = described_class.where(name: %w[fk_constrained_to_referenced fk_constrained_to_other_referenced]).to_a
- expect(described_class.by_constrained_table_name('constrained_table')).to match_array(expected)
+ expect(described_class.by_constrained_table_name(table_name("constrained_table"))).to match_array(expected)
end
end
@@ -80,7 +86,7 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
context 'when finding columns for foreign keys' do
using RSpec::Parameterized::TableSyntax
- let(:fks) { described_class.by_constrained_table_name('constrained_table') }
+ let(:fks) { described_class.by_constrained_table_name(table_name("constrained_table")) }
where(:fk, :expected_constrained, :expected_referenced) do
lazy { described_class.find_by(name: 'fk_constrained_to_referenced') } | %w[referenced_table_id referenced_table_id_b] | %w[id id_b]
@@ -110,25 +116,34 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
end
- describe '#on_delete_action' do
+ describe '#on_delete_action and #on_update_action' do
before do
ApplicationRecord.connection.execute(<<~SQL)
- create table public.referenced_table_all_on_delete_actions (
+ create table #{schema_table_name('referenced_table_all_on_delete_actions')} (
id bigserial primary key not null
);
- create table public.constrained_table_all_on_delete_actions (
+ create table #{schema_table_name('constrained_table_all_on_delete_actions')} (
id bigserial primary key not null,
- ref_id_no_action bigint not null constraint fk_no_action references referenced_table_all_on_delete_actions(id),
- ref_id_restrict bigint not null constraint fk_restrict references referenced_table_all_on_delete_actions(id) on delete restrict,
- ref_id_nullify bigint not null constraint fk_nullify references referenced_table_all_on_delete_actions(id) on delete set null,
- ref_id_cascade bigint not null constraint fk_cascade references referenced_table_all_on_delete_actions(id) on delete cascade,
- ref_id_set_default bigint not null constraint fk_set_default references referenced_table_all_on_delete_actions(id) on delete set default
+ ref_id_no_action bigint not null constraint fk_no_action
+ references #{table_name('referenced_table_all_on_delete_actions')}(id),
+ ref_id_restrict bigint not null constraint fk_restrict
+ references #{table_name('referenced_table_all_on_delete_actions')}(id)
+ on delete restrict on update restrict,
+ ref_id_nullify bigint not null constraint fk_nullify
+ references #{table_name('referenced_table_all_on_delete_actions')}(id)
+ on delete set null on update set null,
+ ref_id_cascade bigint not null constraint fk_cascade
+ references #{table_name('referenced_table_all_on_delete_actions')}(id)
+ on delete cascade on update cascade,
+ ref_id_set_default bigint not null constraint fk_set_default
+ references #{table_name('referenced_table_all_on_delete_actions')}(id)
+ on delete set default on update set default
)
SQL
end
- let(:fks) { described_class.by_constrained_table_name('constrained_table_all_on_delete_actions') }
+ let(:fks) { described_class.by_constrained_table_name(table_name('constrained_table_all_on_delete_actions')) }
context 'with an invalid on_delete_action' do
it 'raises an error' do
@@ -137,7 +152,7 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
end
- where(:fk_name, :expected_on_delete_action) do
+ where(:fk_name, :expected_action) do
[
%w[fk_no_action no_action],
%w[fk_restrict restrict],
@@ -151,12 +166,22 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
subject(:fk) { fks.find_by(name: fk_name) }
it 'has the appropriate on delete action' do
- expect(fk.on_delete_action).to eq(expected_on_delete_action)
+ expect(fk.on_delete_action).to eq(expected_action)
+ end
+
+ it 'has the appropriate on update action' do
+ expect(fk.on_update_action).to eq(expected_action)
end
describe '#by_on_delete_action' do
it 'finds the key by on delete action' do
- expect(fks.by_on_delete_action(expected_on_delete_action)).to contain_exactly(fk)
+ expect(fks.by_on_delete_action(expected_action)).to contain_exactly(fk)
+ end
+ end
+
+ describe '#by_on_update_action' do
+ it 'finds the key by on update action' do
+ expect(fks.by_on_update_action(expected_action)).to contain_exactly(fk)
end
end
end
@@ -167,16 +192,17 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
skip('not supported before postgres 12') if ApplicationRecord.database.version.to_f < 12
ApplicationRecord.connection.execute(<<~SQL)
- create table public.parent (
- id bigserial primary key not null
- ) partition by hash(id);
+ create table #{schema_table_name('parent')} (
+ id bigserial primary key not null
+ ) partition by hash(id);
- create table public.child partition of parent for values with (modulus 2, remainder 1);
+ create table #{schema_table_name('child')} partition of #{table_name('parent')}
+ for values with (modulus 2, remainder 1);
- create table public.referencing_partitioned (
- id bigserial not null primary key,
- constraint fk_inherited foreign key (id) references parent(id)
- )
+ create table #{schema_table_name('referencing_partitioned')} (
+ id bigserial not null primary key,
+ constraint fk_inherited foreign key (id) references #{table_name('parent')}(id)
+ )
SQL
end
@@ -185,7 +211,7 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
where(:fk, :inherited) do
lazy { described_class.find_by(name: 'fk_inherited') } | false
- lazy { described_class.by_referenced_table_identifier('public.child').first! } | true
+ lazy { described_class.by_referenced_table_identifier(schema_table_name('child')).first! } | true
lazy { described_class.find_by(name: 'fk_constrained_to_referenced') } | false
end
@@ -197,12 +223,20 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
end
describe '#not_inherited' do
- let(:fks) { described_class.by_constrained_table_identifier('public.referencing_partitioned') }
+ let(:fks) { described_class.by_constrained_table_identifier(schema_table_name('referencing_partitioned')) }
it 'lists all non-inherited foreign keys' do
- expect(fks.pluck(:referenced_table_name)).to contain_exactly('parent', 'child')
- expect(fks.not_inherited.pluck(:referenced_table_name)).to contain_exactly('parent')
+ expect(fks.pluck(:referenced_table_name)).to contain_exactly(table_name('parent'), table_name('child'))
+ expect(fks.not_inherited.pluck(:referenced_table_name)).to contain_exactly(table_name('parent'))
end
end
end
+
+ def schema_table_name(name)
+ "public.#{table_name(name)}"
+ end
+
+ def table_name(name)
+ "#{table_prefix}_#{name}"
+ end
end
diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb
index db66736676b..d8a2612caf3 100644
--- a/spec/lib/gitlab/database/postgres_index_spec.rb
+++ b/spec/lib/gitlab/database/postgres_index_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
CREATE INDEX #{name} ON public.users (name);
CREATE UNIQUE INDEX bar_key ON public.users (id);
- CREATE TABLE example_table (id serial primary key);
+ CREATE TABLE _test_gitlab_main_example_table (id serial primary key);
SQL
end
@@ -144,7 +144,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do
end
it 'returns true for a primary key index' do
- expect(find('public.example_table_pkey')).to be_unique
+ expect(find('public._test_gitlab_main_example_table_pkey')).to be_unique
end
end
diff --git a/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb b/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb
index 21a46f1a0a6..170cc894071 100644
--- a/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb
+++ b/spec/lib/gitlab/database/postgres_partitioned_table_spec.rb
@@ -3,25 +3,29 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::PostgresPartitionedTable, type: :model do
- let(:schema) { 'public' }
- let(:name) { 'foo_range' }
- let(:identifier) { "#{schema}.#{name}" }
+ let_it_be(:foo_range_table_name) { '_test_gitlab_main_foo_range' }
+ let_it_be(:foo_list_table_name) { '_test_gitlab_main_foo_list' }
+ let_it_be(:foo_hash_table_name) { '_test_gitlab_main_foo_hash' }
- before do
+ let_it_be(:schema) { 'public' }
+ let_it_be(:name) { foo_range_table_name }
+ let_it_be(:identifier) { "#{schema}.#{name}" }
+
+ before_all do
ActiveRecord::Base.connection.execute(<<~SQL)
- CREATE TABLE #{identifier} (
+ CREATE TABLE #{schema}.#{foo_range_table_name} (
id serial NOT NULL,
created_at timestamptz NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE(created_at);
- CREATE TABLE public.foo_list (
+ CREATE TABLE #{schema}.#{foo_list_table_name} (
id serial NOT NULL,
row_type text NOT NULL,
PRIMARY KEY (id, row_type)
) PARTITION BY LIST(row_type);
- CREATE TABLE public.foo_hash (
+ CREATE TABLE #{schema}.#{foo_hash_table_name} (
id serial NOT NULL,
row_value int NOT NULL,
PRIMARY KEY (id, row_value)
@@ -56,31 +60,63 @@ RSpec.describe Gitlab::Database::PostgresPartitionedTable, type: :model do
end
end
+ describe '.each_partition' do
+ context 'without partitions' do
+ it 'does not yield control' do
+ expect { |b| described_class.each_partition(name, &b) }.not_to yield_control
+ end
+ end
+
+ context 'with partitions' do
+ let(:partition_schema) { 'gitlab_partitions_dynamic' }
+ let(:partition1_name) { "#{partition_schema}.#{name}_202001" }
+ let(:partition2_name) { "#{partition_schema}.#{name}_202002" }
+
+ before do
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE TABLE #{partition1_name} PARTITION OF #{identifier}
+ FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
+
+ CREATE TABLE #{partition2_name} PARTITION OF #{identifier}
+ FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
+ SQL
+ end
+
+ it 'yields control with partition as argument' do
+ args = Gitlab::Database::PostgresPartition
+ .where(identifier: [partition1_name, partition2_name])
+ .order(:name).to_a
+
+ expect { |b| described_class.each_partition(name, &b) }.to yield_successive_args(*args)
+ end
+ end
+ end
+
describe '#dynamic?' do
it 'returns true for tables partitioned by range' do
- expect(find('public.foo_range')).to be_dynamic
+ expect(find("#{schema}.#{foo_range_table_name}")).to be_dynamic
end
it 'returns true for tables partitioned by list' do
- expect(find('public.foo_list')).to be_dynamic
+ expect(find("#{schema}.#{foo_list_table_name}")).to be_dynamic
end
it 'returns false for tables partitioned by hash' do
- expect(find('public.foo_hash')).not_to be_dynamic
+ expect(find("#{schema}.#{foo_hash_table_name}")).not_to be_dynamic
end
end
describe '#static?' do
it 'returns false for tables partitioned by range' do
- expect(find('public.foo_range')).not_to be_static
+ expect(find("#{schema}.#{foo_range_table_name}")).not_to be_static
end
it 'returns false for tables partitioned by list' do
- expect(find('public.foo_list')).not_to be_static
+ expect(find("#{schema}.#{foo_list_table_name}")).not_to be_static
end
it 'returns true for tables partitioned by hash' do
- expect(find('public.foo_hash')).to be_static
+ expect(find("#{schema}.#{foo_hash_table_name}")).to be_static
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
index 47038bbd138..d31be6cb883 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
@@ -28,19 +28,19 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
model: ApplicationRecord,
sql: "SELECT 1 FROM projects LEFT JOIN ci_builds ON ci_builds.project_id=projects.id",
expect_error: /The query tried to access \["projects", "ci_builds"\]/,
- setup: -> (_) { skip_if_multiple_databases_not_setup }
+ setup: -> (_) { skip_if_multiple_databases_not_setup(:ci) }
},
"for query accessing gitlab_ci and gitlab_main the gitlab_schemas is always ordered" => {
model: ApplicationRecord,
sql: "SELECT 1 FROM ci_builds LEFT JOIN projects ON ci_builds.project_id=projects.id",
expect_error: /The query tried to access \["ci_builds", "projects"\]/,
- setup: -> (_) { skip_if_multiple_databases_not_setup }
+ setup: -> (_) { skip_if_multiple_databases_not_setup(:ci) }
},
"for query accessing main table from CI database" => {
model: Ci::ApplicationRecord,
sql: "SELECT 1 FROM projects",
expect_error: /The query tried to access \["projects"\]/,
- setup: -> (_) { skip_if_multiple_databases_not_setup }
+ setup: -> (_) { skip_if_multiple_databases_not_setup(:ci) }
},
"for query accessing CI database" => {
model: Ci::ApplicationRecord,
@@ -51,13 +51,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
model: ::ApplicationRecord,
sql: "SELECT 1 FROM ci_builds",
expect_error: /The query tried to access \["ci_builds"\]/,
- setup: -> (_) { skip_if_multiple_databases_not_setup }
+ setup: -> (_) { skip_if_multiple_databases_not_setup(:ci) }
},
"for query accessing unknown gitlab_schema" => {
model: ::ApplicationRecord,
sql: "SELECT 1 FROM new_table",
expect_error: /The query tried to access \["new_table"\] \(of undefined_new_table\)/,
- setup: -> (_) { skip_if_multiple_databases_not_setup }
+ setup: -> (_) { skip_if_multiple_databases_not_setup(:ci) }
}
}
end
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
context "when analyzer is enabled for tests", :query_analyzers do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it "throws an error when trying to access a table that belongs to the gitlab_main schema from the ci database" do
diff --git a/spec/lib/gitlab/database/reflection_spec.rb b/spec/lib/gitlab/database/reflection_spec.rb
index 389e93364c8..779bdbe50f0 100644
--- a/spec/lib/gitlab/database/reflection_spec.rb
+++ b/spec/lib/gitlab/database/reflection_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Reflection do
+RSpec.describe Gitlab::Database::Reflection, feature_category: :database do
let(:database) { described_class.new(ApplicationRecord) }
describe '#username' do
diff --git a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
index bf993e85cb8..9482700df5f 100644
--- a/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/coordinator_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::Database::Reindexing::Coordinator, feature_category: :dat
end
let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
- let(:lease_key) { "gitlab/database/indexing/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
+ let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" }
let(:lease_timeout) { 1.day }
let(:uuid) { 'uuid' }
diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb
index 6575c92e313..a8af9bb5a38 100644
--- a/spec/lib/gitlab/database/reindexing_spec.rb
+++ b/spec/lib/gitlab/database/reindexing_spec.rb
@@ -68,6 +68,25 @@ RSpec.describe Gitlab::Database::Reindexing, feature_category: :database, time_t
end
end
end
+
+ context 'when async FK validation is enabled' do
+ it 'executes FK validation for each database prior to any reindexing actions' do
+ expect(Gitlab::Database::AsyncForeignKeys).to receive(:validate_pending_entries!).ordered.exactly(databases_count).times
+ expect(described_class).to receive(:automatic_reindexing).ordered.exactly(databases_count).times
+
+ described_class.invoke
+ end
+ end
+
+ context 'when async FK validation is disabled' do
+ it 'does not execute FK validation' do
+ stub_feature_flags(database_async_foreign_key_validation: false)
+
+ expect(Gitlab::Database::AsyncForeignKeys).not_to receive(:validate_pending_entries!)
+
+ described_class.invoke
+ end
+ end
end
describe '.automatic_reindexing' do
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 1edcd890370..2cb84e2f02a 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete do
+RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete, feature_category: :subgroups do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
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 c507bce634e..5b5661020b0 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
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete do
+RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete,
+feature_category: :subgroups do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:namespace) { create(:group, name: 'the-path') }
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 aa2a3329477..787c9e87038 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
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete do
+RSpec.describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete,
+feature_category: :projects do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:project) do
diff --git a/spec/lib/gitlab/database/schema_migrations/context_spec.rb b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
index 07c97ea0ec3..6a614e2488f 100644
--- a/spec/lib/gitlab/database/schema_migrations/context_spec.rb
+++ b/spec/lib/gitlab/database/schema_migrations/context_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
let(:connection_class) { Ci::ApplicationRecord }
it 'returns a directory path that is database specific' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect(context.schema_directory).to eq(File.join(Rails.root, described_class.default_schema_migrations_path))
end
@@ -40,7 +40,7 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
end
it 'returns a configured directory path that' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect(context.schema_directory).to eq(File.join(Rails.root, 'db/ci_schema_migrations'))
end
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Database::SchemaMigrations::Context do
end
it 'returns a configured directory path that' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
expect(context.schema_directory).to eq(File.join(Rails.root, 'db/ci_schema_migrations'))
end
diff --git a/spec/lib/gitlab/database/schema_validation/database_spec.rb b/spec/lib/gitlab/database/schema_validation/database_spec.rb
new file mode 100644
index 00000000000..c0026f91b46
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/database_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Database, feature_category: :database do
+ let(:database_name) { 'main' }
+ let(:database_indexes) do
+ [['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']]
+ end
+
+ let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:connection) { database_model.connection }
+
+ subject(:database) { described_class.new(connection) }
+
+ before do
+ allow(connection).to receive(:exec_query).and_return(query_result)
+ end
+
+ describe '#fetch_index_by_name' do
+ context 'when index does not exist' do
+ it 'returns nil' do
+ index = database.fetch_index_by_name('non_existing_index')
+
+ expect(index).to be_nil
+ end
+ end
+
+ it 'returns index by name' do
+ index = database.fetch_index_by_name('index')
+
+ expect(index.name).to eq('index')
+ end
+ end
+
+ describe '#indexes' do
+ it 'returns indexes' do
+ indexes = database.indexes
+
+ expect(indexes).to all(be_a(Gitlab::Database::SchemaValidation::Index))
+ expect(indexes.map(&:name)).to eq(['index'])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/index_spec.rb b/spec/lib/gitlab/database/schema_validation/index_spec.rb
new file mode 100644
index 00000000000..297211d79ed
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/index_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Index, feature_category: :database do
+ let(:index_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' }
+
+ let(:stmt) { PgQuery.parse(index_statement).tree.stmts.first.stmt.index_stmt }
+
+ let(:index) { described_class.new(stmt) }
+
+ describe '#name' do
+ it 'returns index name' do
+ expect(index.name).to eq('index_name')
+ end
+ end
+
+ describe '#statement' do
+ it 'returns index statement' do
+ expect(index.statement).to eq(index_statement)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/schema_validation/indexes_spec.rb b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
new file mode 100644
index 00000000000..4351031a4b4
--- /dev/null
+++ b/spec/lib/gitlab/database/schema_validation/indexes_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::SchemaValidation::Indexes, feature_category: :database do
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:database_indexes) do
+ [
+ ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'],
+ ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'],
+ ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))']
+ ]
+ end
+
+ let(:database_name) { 'main' }
+
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+
+ let(:connection) { database_model.connection }
+
+ let(:query_result) { instance_double('ActiveRecord::Result', rows: database_indexes) }
+
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path) }
+
+ subject(:schema_validation) { described_class.new(structure_file, database) }
+
+ before do
+ allow(connection).to receive(:exec_query).and_return(query_result)
+ end
+
+ describe '#missing_indexes' do
+ it 'returns missing indexes' do
+ missing_indexes = %w[
+ missing_index
+ index_namespaces_public_groups_name_id
+ index_on_deploy_keys_id_and_type_and_public
+ index_users_on_public_email_excluding_null_and_empty
+ ]
+
+ expect(schema_validation.missing_indexes).to match_array(missing_indexes)
+ end
+ end
+
+ describe '#extra_indexes' do
+ it 'returns extra indexes' do
+ expect(schema_validation.extra_indexes).to match_array(['extra_index'])
+ end
+ end
+
+ describe '#wrong_indexes' do
+ it 'returns wrong indexes' do
+ expect(schema_validation.wrong_indexes).to match_array(['wrong_index'])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/shared_model_spec.rb b/spec/lib/gitlab/database/shared_model_spec.rb
index 7e0ba3397d1..2ae6ccf6c6a 100644
--- a/spec/lib/gitlab/database/shared_model_spec.rb
+++ b/spec/lib/gitlab/database/shared_model_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::SharedModel do
+RSpec.describe Gitlab::Database::SharedModel, feature_category: :database do
describe 'using an external connection' do
let!(:original_connection) { described_class.connection }
let(:new_connection) { double('connection') }
@@ -85,28 +85,51 @@ RSpec.describe Gitlab::Database::SharedModel do
end
end
end
-
- def expect_original_connection_around
- # For safety, ensure our original connection is distinct from our double
- # This should be the case, but in case of something leaking we should verify
- expect(original_connection).not_to be(new_connection)
- expect(described_class.connection).to be(original_connection)
-
- yield
-
- expect(described_class.connection).to be(original_connection)
- end
end
describe '#connection_db_config' do
- it 'returns the class connection_db_config' do
- shared_model_class = Class.new(described_class) do
+ let!(:original_connection) { shared_model_class.connection }
+ let!(:original_connection_db_config) { shared_model_class.connection_db_config }
+ let(:shared_model) { shared_model_class.new }
+ let(:shared_model_class) do
+ Class.new(described_class) do
self.table_name = 'postgres_async_indexes'
end
+ end
- shared_model = shared_model_class.new
-
+ it 'returns the class connection_db_config' do
expect(shared_model.connection_db_config).to eq(described_class.connection_db_config)
end
+
+ context 'when switching the class connection' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ let(:new_base_model) { Ci::ApplicationRecord }
+ let(:new_connection) { new_base_model.connection }
+
+ it 'returns the db_config of the used connection when using load balancing' do
+ expect_original_connection_around do
+ described_class.using_connection(new_connection) do
+ expect(shared_model.connection_db_config).to eq(new_base_model.connection_db_config)
+ end
+ end
+
+ # it restores the connection_db_config afterwards
+ expect(shared_model.connection_db_config).to eq(original_connection_db_config)
+ end
+ end
+ end
+
+ def expect_original_connection_around
+ # For safety, ensure our original connection is distinct from our double
+ # This should be the case, but in case of something leaking we should verify
+ expect(original_connection).not_to be(new_connection)
+ expect(described_class.connection).to be(original_connection)
+
+ yield
+
+ expect(described_class.connection).to be(original_connection)
end
end
diff --git a/spec/lib/gitlab/database/tables_locker_spec.rb b/spec/lib/gitlab/database/tables_locker_spec.rb
new file mode 100644
index 00000000000..d74f455eaad
--- /dev/null
+++ b/spec/lib/gitlab/database/tables_locker_spec.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::TablesLocker, :reestablished_active_record_base, :delete, :silence_stdout,
+ :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
+ let(:detached_partition_table) { '_test_gitlab_main_part_20220101' }
+ let(:lock_writes_manager) do
+ instance_double(Gitlab::Database::LockWritesManager, lock_writes: nil, unlock_writes: nil)
+ end
+
+ before do
+ allow(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
+ end
+
+ before(:all) 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
+
+ ApplicationRecord.connection.execute(create_detached_partition_sql)
+ Ci::ApplicationRecord.connection.execute(create_detached_partition_sql)
+
+ Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
+ Postgresql::DetachedPartition.create!(
+ table_name: '_test_gitlab_main_part_20220101',
+ drop_after: Time.current
+ )
+ end
+ end
+
+ after(:all) do
+ drop_detached_partition_sql = <<~SQL
+ DROP TABLE IF EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101
+ SQL
+
+ ApplicationRecord.connection.execute(drop_detached_partition_sql)
+ Ci::ApplicationRecord.connection.execute(drop_detached_partition_sql)
+
+ Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
+ Postgresql::DetachedPartition.delete_all
+ end
+ end
+
+ shared_examples "lock tables" do |table_schema, database_name|
+ let(:table_name) do
+ Gitlab::Database::GitlabSchema
+ .tables_to_schema.filter_map { |table_name, schema| table_name if schema == table_schema }
+ .first
+ end
+
+ let(:database) { database_name }
+
+ it "locks table in schema #{table_schema} and database #{database_name}" do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
+ table_name: table_name,
+ connection: anything,
+ database_name: database,
+ with_retries: true,
+ logger: anything,
+ dry_run: anything
+ ).once.and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:lock_writes)
+
+ subject
+ end
+ end
+
+ shared_examples "unlock tables" do |table_schema, database_name|
+ let(:table_name) do
+ Gitlab::Database::GitlabSchema
+ .tables_to_schema.filter_map { |table_name, schema| table_name if schema == table_schema }
+ .first
+ end
+
+ let(:database) { database_name }
+
+ it "unlocks table in schema #{table_schema} and database #{database_name}" do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
+ table_name: table_name,
+ connection: anything,
+ database_name: database,
+ with_retries: true,
+ logger: anything,
+ dry_run: anything
+ ).once.and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:unlock_writes)
+
+ subject
+ end
+ end
+
+ context 'when running on single database' do
+ before do
+ skip_if_multiple_databases_are_setup(:ci)
+ end
+
+ describe '#lock_writes' do
+ subject { described_class.new.lock_writes }
+
+ it 'does not call Gitlab::Database::LockWritesManager.lock_writes' do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
+ expect(lock_writes_manager).not_to receive(:lock_writes)
+
+ subject
+ end
+
+ include_examples "unlock tables", :gitlab_main, 'main'
+ include_examples "unlock tables", :gitlab_ci, 'ci'
+ include_examples "unlock tables", :gitlab_shared, 'main'
+ include_examples "unlock tables", :gitlab_internal, 'main'
+ end
+
+ describe '#unlock_writes' do
+ subject { described_class.new.lock_writes }
+
+ it 'does call Gitlab::Database::LockWritesManager.unlock_writes' do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:unlock_writes)
+
+ subject
+ end
+ end
+ end
+
+ context 'when running on multiple databases' do
+ before do
+ skip_if_multiple_databases_not_setup(:ci)
+ end
+
+ describe '#lock_writes' do
+ subject { described_class.new.lock_writes }
+
+ include_examples "lock tables", :gitlab_ci, 'main'
+ include_examples "lock tables", :gitlab_main, 'ci'
+
+ include_examples "unlock tables", :gitlab_main, 'main'
+ include_examples "unlock tables", :gitlab_ci, 'ci'
+ include_examples "unlock tables", :gitlab_shared, 'main'
+ include_examples "unlock tables", :gitlab_shared, 'ci'
+ include_examples "unlock tables", :gitlab_internal, 'main'
+ include_examples "unlock tables", :gitlab_internal, 'ci'
+ end
+
+ describe '#unlock_writes' do
+ subject { described_class.new.unlock_writes }
+
+ include_examples "unlock tables", :gitlab_ci, 'main'
+ include_examples "unlock tables", :gitlab_main, 'ci'
+ include_examples "unlock tables", :gitlab_main, 'main'
+ include_examples "unlock tables", :gitlab_ci, 'ci'
+ include_examples "unlock tables", :gitlab_shared, 'main'
+ include_examples "unlock tables", :gitlab_shared, 'ci'
+ include_examples "unlock tables", :gitlab_internal, 'main'
+ include_examples "unlock tables", :gitlab_internal, 'ci'
+ end
+
+ context 'when running in dry_run mode' do
+ subject { described_class.new(dry_run: true).lock_writes }
+
+ it 'passes dry_run flag to LockManger' do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(
+ table_name: 'users',
+ connection: anything,
+ database_name: 'ci',
+ with_retries: true,
+ logger: anything,
+ dry_run: true
+ ).and_return(lock_writes_manager)
+ expect(lock_writes_manager).to receive(:lock_writes)
+
+ subject
+ end
+ end
+
+ context 'when running on multiple shared databases' do
+ subject { described_class.new.lock_writes }
+
+ before do
+ allow(::Gitlab::Database).to receive(:db_config_share_with).and_return(nil)
+ ci_db_config = Ci::ApplicationRecord.connection_db_config
+ allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
+ end
+
+ it 'does not lock any tables if the ci database is shared with main database' do
+ expect(Gitlab::Database::LockWritesManager).to receive(:new).with(any_args).and_return(lock_writes_manager)
+ expect(lock_writes_manager).not_to receive(:lock_writes)
+
+ subject
+ end
+ end
+ end
+
+ context 'when geo database is configured' do
+ let(:geo_table) do
+ Gitlab::Database::GitlabSchema
+ .tables_to_schema.filter_map { |table_name, schema| table_name if schema == :gitlab_geo }
+ .first
+ end
+
+ subject { described_class.new.unlock_writes }
+
+ before do
+ skip "Geo database is not configured" unless Gitlab::Database.has_config?(:geo)
+ end
+
+ it 'does not lock table in geo database' do
+ expect(Gitlab::Database::LockWritesManager).not_to receive(:new).with(
+ table_name: geo_table,
+ connection: anything,
+ database_name: 'geo',
+ with_retries: true,
+ logger: anything,
+ dry_run: anything
+ )
+
+ subject
+ end
+ end
+end
+
+def number_of_triggers(connection)
+ connection.select_value("SELECT count(*) FROM information_schema.triggers")
+end
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index 9af0b964221..3bb2f4e982c 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
end
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
# Creating some test tables on the main database
main_tables_sql = <<~SQL
@@ -313,7 +313,7 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'when running with multiple shared databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
ci_db_config = Ci::ApplicationRecord.connection_db_config
allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
end
@@ -333,7 +333,7 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
context 'when running in a single database mode' do
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'raises an error when truncating the main database that it is a single database setup' do
diff --git a/spec/lib/gitlab/database/transaction/observer_spec.rb b/spec/lib/gitlab/database/transaction/observer_spec.rb
index 074c18d406e..d1cb014a594 100644
--- a/spec/lib/gitlab/database/transaction/observer_spec.rb
+++ b/spec/lib/gitlab/database/transaction/observer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::Transaction::Observer do
+RSpec.describe Gitlab::Database::Transaction::Observer, feature_category: :database do
# Use the delete DB strategy so that the test won't be wrapped in a transaction
describe '.instrument_transactions', :delete do
let(:transaction_context) { ActiveRecord::Base.connection.transaction_manager.transaction_context }
diff --git a/spec/lib/gitlab/database/transaction_timeout_settings_spec.rb b/spec/lib/gitlab/database/transaction_timeout_settings_spec.rb
new file mode 100644
index 00000000000..5b68f9a3757
--- /dev/null
+++ b/spec/lib/gitlab/database/transaction_timeout_settings_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::TransactionTimeoutSettings, feature_category: :pods do
+ let(:connection) { ActiveRecord::Base.connection }
+
+ subject { described_class.new(connection) }
+
+ after(:all) do
+ described_class.new(ActiveRecord::Base.connection).restore_timeouts
+ end
+
+ describe '#disable_timeouts' do
+ it 'sets timeouts to 0' do
+ subject.disable_timeouts
+
+ expect(current_timeout).to eq("0")
+ end
+ end
+
+ describe '#restore_timeouts' do
+ before do
+ subject.disable_timeouts
+ end
+
+ it 'resets value' do
+ expect(connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout').and_call_original
+
+ subject.restore_timeouts
+ end
+ end
+
+ def current_timeout
+ connection.execute("show idle_in_transaction_session_timeout").first['idle_in_transaction_session_timeout']
+ end
+end
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
index 836332524a9..9ccae754a92 100644
--- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
+RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction, feature_category: :database do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
let(:subject) { described_class.new(connection: connection, env: env, logger: logger, timing_configuration: timing_configuration) }
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 797a01c482d..7fe6362634b 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::WithLockRetries do
+RSpec.describe Gitlab::Database::WithLockRetries, feature_category: :database do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
let(:subject) { described_class.new(connection: connection, env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
index d67e50a50d4..d878d46c883 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
@@ -38,6 +38,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::DeleteService
it 'deletes project ID from application settings' do
subject.execute
+ LooseForeignKeys::ProcessDeletedRecordsService.new(connection: Project.connection).execute
+
expect(application_setting.reload.self_monitoring_project_id).to be_nil
end
diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
index d044170dc75..3b6d10f4a7e 100644
--- a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
+++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do
+RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter, feature_category: :team_planning do
subject { described_class.upsert_types }
it_behaves_like 'work item base types importer'
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 86bc8e71fd7..26d6ff431ec 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -33,20 +33,31 @@ RSpec.describe Gitlab::Database do
describe '.has_config?' do
context 'three tier database config' do
- before do
- allow(Gitlab::Application).to receive_message_chain(:config, :database_configuration, :[]).with(Rails.env)
- .and_return({
- "primary" => { "adapter" => "postgresql", "database" => "gitlabhq_test" },
- "ci" => { "adapter" => "postgresql", "database" => "gitlabhq_test_ci" }
- })
+ it 'returns true for main' do
+ expect(described_class.has_config?(:main)).to eq(true)
end
- it 'returns true for primary' do
- expect(described_class.has_config?(:primary)).to eq(true)
- end
+ context 'ci' do
+ before do
+ # CI config might not be configured
+ allow(ActiveRecord::Base.configurations).to receive(:configs_for)
+ .with(env_name: 'test', name: 'ci', include_replicas: true)
+ .and_return(ci_db_config)
+ end
+
+ let(:ci_db_config) { instance_double('ActiveRecord::DatabaseConfigurations::HashConfig') }
+
+ it 'returns true for ci' do
+ expect(described_class.has_config?(:ci)).to eq(true)
+ end
- it 'returns true for ci' do
- expect(described_class.has_config?(:ci)).to eq(true)
+ context 'ci database.yml not configured' do
+ let(:ci_db_config) { nil }
+
+ it 'returns false for ci' do
+ expect(described_class.has_config?(:ci)).to eq(false)
+ end
+ end
end
it 'returns false for non-existent' do
@@ -189,7 +200,7 @@ RSpec.describe Gitlab::Database do
end
it 'returns the ci_replica for a ci database replica' do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
replica = Ci::ApplicationRecord.load_balancer.host
expect(described_class.db_config_name(replica)).to eq('ci_replica')
end
@@ -256,7 +267,7 @@ RSpec.describe Gitlab::Database do
context "when there's CI connection" do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
context 'when CI uses database_tasks: false does indicate that ci: is subset of main:' do
@@ -516,4 +527,33 @@ RSpec.describe Gitlab::Database do
end
end
end
+
+ describe '.read_minimum_migration_version' do
+ before do
+ allow(Dir).to receive(:open).with(Rails.root.join('db/migrate')).and_return(migration_files)
+ end
+
+ context 'valid migration files exist' do
+ let(:migration_files) do
+ [
+ '20211004170422_init_schema.rb',
+ '20211005182304_add_users.rb'
+ ]
+ end
+
+ let(:valid_schema) { 20211004170422 }
+
+ it 'finds the correct ID' do
+ expect(described_class.read_minimum_migration_version).to eq valid_schema
+ end
+ end
+
+ context 'no valid migration files exist' do
+ let(:migration_files) { ['readme.txt', 'INSTALL'] }
+
+ it 'returns nil' do
+ expect(described_class.read_minimum_migration_version).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/deploy_key_access_spec.rb b/spec/lib/gitlab/deploy_key_access_spec.rb
index 83b97c8ba25..e32858cc13f 100644
--- a/spec/lib/gitlab/deploy_key_access_spec.rb
+++ b/spec/lib/gitlab/deploy_key_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::DeployKeyAccess do
+RSpec.describe Gitlab::DeployKeyAccess, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:deploy_key) { create(:deploy_key, user: user) }
@@ -17,10 +17,30 @@ RSpec.describe Gitlab::DeployKeyAccess do
end
describe '#can_create_tag?' do
+ let!(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project, name: 'v*') }
+
+ context 'when no-one can create tag' do
+ it 'returns false' do
+ expect(access.can_create_tag?('v0.1.2')).to be_falsey
+ end
+
+ context 'when deploy_key_for_protected_tags FF is disabled' do
+ before do
+ stub_feature_flags(deploy_key_for_protected_tags: false)
+ end
+
+ it 'allows to push the tag' do
+ expect(access.can_create_tag?('v0.1.2')).to be_truthy
+ end
+ end
+ end
+
context 'push tag that matches a protected tag pattern via a deploy key' do
- it 'still pushes that tag' do
- create(:protected_tag, project: project, name: 'v*')
+ before do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ end
+ it 'allows to push the tag' do
expect(access.can_create_tag?('v0.1.2')).to be_truthy
end
end
diff --git a/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb
new file mode 100644
index 00000000000..fe585d47d59
--- /dev/null
+++ b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::HtmlToMarkdownParser, feature_category: :service_desk do
+ subject { described_class.convert(html) }
+
+ describe '.convert' do
+ let(:html) { fixture_file("lib/gitlab/email/basic.html") }
+
+ it 'parses html correctly' do
+ expect(subject)
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ Hello, World!
+ This is some e-mail content. Even though it has whitespace and newlines, the e-mail converter will handle it correctly.
+ *Even* mismatched tags.
+ A div
+ Another div
+ A div
+ **within** a div
+
+ Another line
+ Yet another line
+ [A link](http://foo.com)
+ <details>
+ <summary>
+ One</summary>
+ Some details</details>
+
+ <details>
+ <summary>
+ Two</summary>
+ Some details</details>
+
+ ![Miro](http://img.png)
+ Col A Col B
+ Data A1 Data B1
+ Data A2 Data B2
+ Data A3 Data B4
+ Total A Total B
+ BODY
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index c61d941406b..e4c68dbba92 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -63,6 +63,18 @@ RSpec.describe Gitlab::Email::ReplyParser do
)
end
+ it "properly renders html-only email with table and blockquote" do
+ expect(test_parse_body(fixture_file("emails/html_table_and_blockquote.eml")))
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ Company Contact Country
+ Alfreds Futterkiste Maria Anders Germany
+ Centro comercial Moctezuma Francisco Chang Mexico
+ Words can be like X-rays, if you use them properly—they’ll go through anything. You read and you’re pierced.
+ BODY
+ )
+ end
+
it "supports a Dutch reply" do
expect(test_parse_body(fixture_file("emails/dutch.eml"))).to eq("Dit is een antwoord in het Nederlands.")
end
@@ -176,6 +188,70 @@ RSpec.describe Gitlab::Email::ReplyParser do
)
end
+ context 'properly renders email reply from gmail web client' do
+ context 'when feature flag is enabled' do
+ it do
+ expect(test_parse_body(fixture_file("emails/html_only.eml")))
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### This is a reply from standard GMail in Google Chrome.
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+
+ Here's some **bold** text, **strong** text and *italic* in Markdown.
+
+ Here's a link http://example.com
+
+ Here's an img ![Miro](http://img.png)<details>
+ <summary>
+ One</summary>
+ Some details</details>
+
+ <details>
+ <summary>
+ Two</summary>
+ Some details</details>
+
+ Test reply.
+
+ First paragraph.
+
+ Second paragraph.
+ BODY
+ )
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(service_desk_html_to_text_email_handler: false)
+ end
+
+ it do
+ expect(test_parse_body(fixture_file("emails/html_only.eml")))
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### This is a reply from standard GMail in Google Chrome.
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+
+ Here's some **bold** text, strong text and italic in Markdown.
+
+ Here's a link http://example.com
+
+ Here's an img [Miro]One Some details Two Some details
+
+ Test reply.
+
+ First paragraph.
+
+ Second paragraph.
+ BODY
+ )
+ end
+ end
+ end
+
it "properly renders email reply from iOS default mail client" do
expect(test_parse_body(fixture_file("emails/ios_default.eml")))
.to eq(
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index c62e3071fc1..bc72d1a67d6 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -283,4 +283,12 @@ RSpec.describe Gitlab::EncodingHelper do
expect(described_class.unquote_path('"\a\b\e\f\n\r\t\v\""')).to eq("\a\b\e\f\n\r\t\v\"")
end
end
+
+ describe '#strip_bom' do
+ it do
+ expect(described_class.strip_bom('no changes')).to eq('no changes')
+ expect(described_class.strip_bom("\xEF\xBB\xBFhello world")).to eq('hello world')
+ expect(described_class.strip_bom("BOM at the end\xEF\xBB\xBF")).to eq("BOM at the end\xEF\xBB\xBF")
+ end
+ end
end
diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb
index 5eedd716a4a..0f056ee9eac 100644
--- a/spec/lib/gitlab/error_tracking_spec.rb
+++ b/spec/lib/gitlab/error_tracking_spec.rb
@@ -490,4 +490,28 @@ RSpec.describe Gitlab::ErrorTracking do
end
end
end
+
+ context 'Sentry performance monitoring' do
+ context 'when ENABLE_SENTRY_PERFORMANCE_MONITORING env is disabled' do
+ before do
+ stub_env('ENABLE_SENTRY_PERFORMANCE_MONITORING', false)
+ described_class.configure_sentry # Force re-initialization to reset traces_sample_rate setting
+ end
+
+ it 'does not set traces_sample_rate' do
+ expect(Sentry.get_current_client.configuration.traces_sample_rate.present?).to eq false
+ end
+ end
+
+ context 'when ENABLE_SENTRY_PERFORMANCE_MONITORING env is enabled' do
+ before do
+ stub_env('ENABLE_SENTRY_PERFORMANCE_MONITORING', true)
+ described_class.configure_sentry # Force re-initialization to reset traces_sample_rate setting
+ end
+
+ it 'sets traces_sample_rate' do
+ expect(Sentry.get_current_client.configuration.traces_sample_rate.present?).to eq true
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index da5eaf2e4ab..fa0b3d1c6dd 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -123,7 +123,15 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
format: :html,
method: 'GET',
path: enabled_path,
- status: status_code
+ status: status_code,
+ request_urgency: :medium,
+ target_duration_s: 0.5,
+ metadata: a_hash_including(
+ {
+ 'meta.caller_id' => 'Projects::NotesController#index',
+ 'meta.feature_category' => 'team_planning'
+ }
+ )
}
end
@@ -172,10 +180,11 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
expect(headers).to include('X-Gitlab-From-Cache' => 'true')
end
- it "pushes route's feature category to the context" do
+ it "pushes expected information in to the context" do
expect(Gitlab::ApplicationContext).to receive(:push).with(
feature_category: 'team_planning',
- caller_id: 'Projects::NotesController#index'
+ caller_id: 'Projects::NotesController#index',
+ remote_ip: '127.0.0.1'
)
_, _, _ = middleware.call(build_request(path, if_none_match))
@@ -291,7 +300,8 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
{ 'PATH_INFO' => path,
'HTTP_IF_NONE_MATCH' => if_none_match,
'rack.input' => '',
- 'REQUEST_METHOD' => 'GET' }
+ 'REQUEST_METHOD' => 'GET',
+ 'REMOTE_ADDR' => '127.0.0.1' }
end
def payload_for(event)
diff --git a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
index 792f02f8cda..fae468ab84f 100644
--- a/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router/graphql_spec.rb
@@ -18,6 +18,12 @@ RSpec.describe Gitlab::EtagCaching::Router::Graphql do
end
end
+ it 'applies the default urgency for every route', :aggregate_failures do
+ described_class::ROUTES.each do |route|
+ expect(route.urgency).to be(Gitlab::EndpointAttributes::DEFAULT_URGENCY)
+ end
+ end
+
def match_route(path, header)
described_class.match(
double(path_info: path,
diff --git a/spec/lib/gitlab/etag_caching/router/rails_spec.rb b/spec/lib/gitlab/etag_caching/router/rails_spec.rb
index da6c11e3cb1..251f634aac1 100644
--- a/spec/lib/gitlab/etag_caching/router/rails_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router/rails_spec.rb
@@ -109,17 +109,23 @@ RSpec.describe Gitlab::EtagCaching::Router::Rails do
it 'has a valid feature category for every route', :aggregate_failures do
feature_categories = Gitlab::FeatureCategories.default.categories
- described_class::ROUTES.each do |route|
+ described_class.all_routes.each do |route|
expect(feature_categories).to include(route.feature_category), "#{route.name} has a category of #{route.feature_category}, which is not valid"
end
end
it 'has a caller_id for every route', :aggregate_failures do
- described_class::ROUTES.each do |route|
+ described_class.all_routes.each do |route|
expect(route.caller_id).to include('#'), "#{route.name} has caller_id #{route.caller_id}, which is not valid"
end
end
+ it 'has an urgency for every route', :aggregate_failures do
+ described_class.all_routes.each do |route|
+ expect(route.urgency).to be_an_instance_of(Gitlab::EndpointAttributes::Config::RequestUrgency)
+ end
+ end
+
def match_route(path)
described_class.match(double(path_info: path))
end
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index 8d2183bc03d..bc07f9a99a5 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe Gitlab::EtagCaching::Router do
expect(result).to be_present
expect(result.name).to eq 'project_pipelines'
expect(result.router).to eq Gitlab::EtagCaching::Router::Rails
+ expect(result.urgency).to eq Projects::PipelinesController.urgency_for_action(:index)
end
end
@@ -21,6 +22,7 @@ RSpec.describe Gitlab::EtagCaching::Router do
expect(result).to be_present
expect(result.name).to eq 'pipelines_graph'
expect(result.router).to eq Gitlab::EtagCaching::Router::Graphql
+ expect(result.urgency).to eq ::Gitlab::EndpointAttributes::DEFAULT_URGENCY
end
it 'matches pipeline sha endpoint' do
diff --git a/spec/lib/gitlab/external_authorization/config_spec.rb b/spec/lib/gitlab/external_authorization/config_spec.rb
new file mode 100644
index 00000000000..4231b0d3747
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/config_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ExternalAuthorization::Config, feature_category: :authentication_and_authorization do
+ it 'allows deploy tokens and keys when external authorization is disabled' do
+ stub_application_setting(external_authorization_service_enabled: false)
+ expect(described_class.allow_deploy_tokens_and_deploy_keys?).to be_eql(true)
+ end
+
+ context 'when external authorization is enabled' do
+ it 'disable deploy tokens and keys' do
+ stub_application_setting(external_authorization_service_enabled: true)
+ expect(described_class.allow_deploy_tokens_and_deploy_keys?).to be_eql(false)
+ end
+
+ it "enable deploy tokens and keys when it is explicitly enabled and service url is blank" do
+ stub_application_setting(external_authorization_service_enabled: true)
+ stub_application_setting(allow_deploy_tokens_and_keys_with_external_authn: true)
+ expect(described_class.allow_deploy_tokens_and_deploy_keys?).to be_eql(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 0b5303f22b4..27750f10e87 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe Gitlab::FileFinder do
+RSpec.describe Gitlab::FileFinder, feature_category: :global_search do
describe '#find' do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
subject { described_class.new(project, project.default_branch) }
@@ -13,58 +13,124 @@ RSpec.describe Gitlab::FileFinder do
let(:expected_file_by_content) { 'CHANGELOG' }
end
- context 'with inclusive filters' do
- it 'filters by filename' do
- results = subject.find('files filename:wm.svg')
+ context 'when code_basic_search_files_by_regexp is enabled' do
+ before do
+ stub_feature_flags(code_basic_search_files_by_regexp: true)
+ end
+
+ context 'with inclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files filename:wm.svg')
+
+ expect(results.count).to eq(1)
+ end
+
+ it 'filters by path' do
+ results = subject.find('white path:images')
- expect(results.count).to eq(1)
+ expect(results.count).to eq(2)
+ end
+
+ it 'filters by extension' do
+ results = subject.find('files extension:md')
+
+ expect(results.count).to eq(4)
+ end
end
- it 'filters by path' do
- results = subject.find('white path:images')
+ context 'with exclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files -filename:wm.svg')
+
+ expect(results.count).to eq(26)
+ end
- expect(results.count).to eq(1)
+ it 'filters by path' do
+ results = subject.find('white -path:images')
+
+ expect(results.count).to eq(5)
+ end
+
+ it 'filters by extension' do
+ results = subject.find('files -extension:md')
+
+ expect(results.count).to eq(23)
+ end
end
- it 'filters by extension' do
- results = subject.find('files extension:md')
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
- expect(results.count).to eq(4)
+ expect(results.count).to eq(1)
+ end
end
- end
- context 'with exclusive filters' do
- it 'filters by filename' do
- results = subject.find('files -filename:wm.svg')
+ it 'does not cause N+1 query' do
+ expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
- expect(results.count).to eq(26)
+ subject.find(': filename:wm.svg')
end
+ end
- it 'filters by path' do
- results = subject.find('white -path:images')
+ context 'when code_basic_search_files_by_regexp is disabled' do
+ before do
+ stub_feature_flags(code_basic_search_files_by_regexp: false)
+ end
- expect(results.count).to eq(4)
+ context 'with inclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files filename:wm.svg')
+
+ expect(results.count).to eq(1)
+ end
+
+ it 'filters by path' do
+ results = subject.find('white path:images')
+
+ expect(results.count).to eq(1)
+ end
+
+ it 'filters by extension' do
+ results = subject.find('files extension:md')
+
+ expect(results.count).to eq(4)
+ end
end
- it 'filters by extension' do
- results = subject.find('files -extension:md')
+ context 'with exclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files -filename:wm.svg')
+
+ expect(results.count).to eq(26)
+ end
- expect(results.count).to eq(23)
+ it 'filters by path' do
+ results = subject.find('white -path:images')
+
+ expect(results.count).to eq(4)
+ end
+
+ it 'filters by extension' do
+ results = subject.find('files -extension:md')
+
+ expect(results.count).to eq(23)
+ end
end
- end
- context 'with white space in the path' do
- it 'filters by path correctly' do
- results = subject.find('directory path:"with space/README.md"')
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
- expect(results.count).to eq(1)
+ expect(results.count).to eq(1)
+ end
end
- end
- it 'does not cause N+1 query' do
- expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
+ it 'does not cause N+1 query' do
+ expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
- subject.find(': filename:wm.svg')
+ subject.find(': filename:wm.svg')
+ end
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 6cff39c1167..72043ba2a21 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1947,6 +1947,53 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
expect(reference.name).to be_a(String)
expect(reference.target).to be_a(String)
end
+
+ it 'filters by pattern' do
+ refs = repository.list_refs([Gitlab::Git::TAG_REF_PREFIX])
+
+ refs.each do |reference|
+ expect(reference.name).to include(Gitlab::Git::TAG_REF_PREFIX)
+ end
+ end
+
+ context 'with pointing_at_oids and peel_tags options' do
+ let(:commit_id) { mutable_repository.commit.id }
+ let!(:annotated_tag) { mutable_repository.add_tag('annotated-tag', user: user, target: commit_id, message: 'Tag message') }
+ let!(:lw_tag) { mutable_repository.add_tag('lw-tag', user: user, target: commit_id) }
+
+ it 'filters by target OIDs' do
+ refs = mutable_repository.list_refs([Gitlab::Git::TAG_REF_PREFIX], pointing_at_oids: [commit_id])
+
+ expect(refs.length).to eq(2)
+ expect(refs).to contain_exactly(
+ Gitaly::ListRefsResponse::Reference.new(
+ name: "#{Gitlab::Git::TAG_REF_PREFIX}#{lw_tag.name}",
+ target: commit_id
+ ),
+ Gitaly::ListRefsResponse::Reference.new(
+ name: "#{Gitlab::Git::TAG_REF_PREFIX}#{annotated_tag.name}",
+ target: annotated_tag.id
+ )
+ )
+ end
+
+ it 'returns peeled_target for annotated tags' do
+ refs = mutable_repository.list_refs([Gitlab::Git::TAG_REF_PREFIX], pointing_at_oids: [commit_id], peel_tags: true)
+
+ expect(refs.length).to eq(2)
+ expect(refs).to contain_exactly(
+ Gitaly::ListRefsResponse::Reference.new(
+ name: "#{Gitlab::Git::TAG_REF_PREFIX}#{lw_tag.name}",
+ target: commit_id
+ ),
+ Gitaly::ListRefsResponse::Reference.new(
+ name: "#{Gitlab::Git::TAG_REF_PREFIX}#{annotated_tag.name}",
+ target: annotated_tag.id,
+ peeled_target: commit_id
+ )
+ )
+ end
+ end
end
describe '#refs_by_oid' do
@@ -2059,16 +2106,22 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
let(:repository) { mutable_repository }
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
let(:target_branch) { 'test-merge-target-branch' }
+ let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
before do
- repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f')
+ repository.create_branch(target_branch, target_sha)
end
it 'can perform a merge' do
merge_commit_id = nil
- result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id|
- merge_commit_id = commit_id
- end
+ result =
+ repository.merge(user,
+ source_sha: source_sha,
+ target_branch: target_branch,
+ target_sha: target_sha,
+ message: 'Test merge') do |commit_id|
+ merge_commit_id = commit_id
+ end
expect(result.newrev).to eq(merge_commit_id)
expect(result.repo_created).to eq(false)
@@ -2077,10 +2130,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it 'returns nil if there was a concurrent branch update' do
concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
- result = repository.merge(user, source_sha, target_branch, 'Test merge') do
- # This ref update should make the merge fail
- repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
- end
+ result =
+ repository.merge(user,
+ source_sha: source_sha,
+ target_branch: target_branch,
+ target_sha: target_sha,
+ message: 'Test merge') do |_commit_id|
+ # This ref update should make the merge fail
+ repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
+ end
# This 'nil' signals that the merge was not applied
expect(result).to be_nil
@@ -2100,7 +2158,13 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
repository.create_branch(target_branch, branch_head)
end
- subject { repository.ff_merge(user, source_sha, target_branch) }
+ subject do
+ repository.ff_merge(user,
+ source_sha: source_sha,
+ target_branch: target_branch,
+ target_sha: branch_head
+ )
+ end
shared_examples '#ff_merge' do
it 'performs a ff_merge' do
@@ -2112,7 +2176,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
context 'with a non-existing target branch' do
- subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
+ subject { repository.ff_merge(user, source_sha: source_sha, target_branch: 'this-isnt-real') }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
@@ -2140,8 +2204,9 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
it "calls Gitaly's OperationService" do
expect_any_instance_of(Gitlab::GitalyClient::OperationService)
- .to receive(:user_ff_branch).with(user, source_sha, target_branch)
- .and_return(nil)
+ .to receive(:user_ff_branch).with(
+ user, source_sha: source_sha, target_branch: target_branch, target_sha: branch_head
+ ).and_return(nil)
subject
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 1b8da0b380b..c5b44b260c6 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, feature_category: :gitlay do
+RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, feature_category: :gitaly do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 10a099af4f0..ea2c239df07 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitAccess, :aggregate_failures do
+RSpec.describe Gitlab::GitAccess, :aggregate_failures, feature_category: :authentication_and_authorization do
include TermsHelper
include AdminModeHelper
include ExternalAuthorizationServiceHelpers
@@ -113,9 +113,9 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures do
end
end
- context 'when the external_authorization_service is enabled' do
+ context 'when the the deploy key is restricted with external_authorization' do
before do
- stub_application_setting(external_authorization_service_enabled: true)
+ allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false)
end
it 'blocks push and pull with "not found"' do
@@ -191,9 +191,9 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures do
end
end
- context 'when the external_authorization_service is enabled' do
+ context 'when the the deploy token is restricted with external_authorization' do
before do
- stub_application_setting(external_authorization_service_enabled: true)
+ allow(Gitlab::ExternalAuthorization).to receive(:allow_deploy_tokens_and_deploy_keys?).and_return(false)
end
it 'blocks pull access' do
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ff3cade07c0..252d20d9c3a 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -63,6 +63,47 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end
end
+ context 'when given a whitespace param' do
+ context 'and the param is true' do
+ it 'uses the ignore all white spaces const' do
+ request = Gitaly::CommitDiffRequest.new
+
+ expect(Gitaly::CommitDiffRequest).to receive(:new)
+ .with(hash_including(whitespace_changes: Gitaly::CommitDiffRequest::WhitespaceChanges::WHITESPACE_CHANGES_IGNORE_ALL)).and_return(request)
+
+ expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
+
+ client.diff_from_parent(commit, ignore_whitespace_change: true)
+ end
+ end
+
+ context 'and the param is false' do
+ it 'does not set a whitespace param' do
+ request = Gitaly::CommitDiffRequest.new
+
+ expect(Gitaly::CommitDiffRequest).to receive(:new)
+ .with(hash_not_including(:whitespace_changes)).and_return(request)
+
+ expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
+
+ client.diff_from_parent(commit, ignore_whitespace_change: false)
+ end
+ end
+ end
+
+ context 'when given no whitespace param' do
+ it 'does not set a whitespace param' do
+ request = Gitaly::CommitDiffRequest.new
+
+ expect(Gitaly::CommitDiffRequest).to receive(:new)
+ .with(hash_not_including(:whitespace_changes)).and_return(request)
+
+ expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
+
+ client.diff_from_parent(commit)
+ end
+ end
+
it 'returns a Gitlab::GitalyClient::DiffStitcher' do
ret = client.diff_from_parent(commit)
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 82d5d0f292b..84672eb81c0 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::OperationService do
+RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source_code_management do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@@ -42,21 +42,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
expect(subject.dereferenced_target).to eq(commit)
end
- context "when pre_receive_error is present" do
- let(:response) do
- Gitaly::UserCreateBranchResponse.new(pre_receive_error: "GitLab: something failed")
- end
-
- it "throws a PreReceive exception" do
- expect_any_instance_of(Gitaly::OperationService::Stub)
- .to receive(:user_create_branch).with(request, kind_of(Hash))
- .and_return(response)
-
- expect { subject }.to raise_error(
- Gitlab::Git::PreReceiveError, "something failed")
- end
- end
-
context 'with structured errors' do
context 'with CustomHookError' do
let(:stdout) { nil }
@@ -232,21 +217,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
subject
end
- context "when pre_receive_error is present" do
- let(:response) do
- Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "GitLab: something failed")
- end
-
- it "throws a PreReceive exception" do
- expect_any_instance_of(Gitaly::OperationService::Stub)
- .to receive(:user_delete_branch).with(request, kind_of(Hash))
- .and_return(response)
-
- expect { subject }.to raise_error(
- Gitlab::Git::PreReceiveError, "something failed")
- end
- end
-
context 'with a custom hook error' do
let(:stdout) { nil }
let(:stderr) { nil }
@@ -309,12 +279,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
describe '#user_merge_branch' do
let(:target_branch) { 'master' }
+ let(:target_sha) { repository.commit(target_branch).sha }
let(:source_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
let(:message) { 'Merge a branch' }
- subject { client.user_merge_branch(user, source_sha, target_branch, message) {} }
+ subject do
+ client.user_merge_branch(user,
+ source_sha: source_sha,
+ target_branch: target_branch,
+ target_sha: target_sha,
+ message: message
+ ) {}
+ end
+
+ it 'sends a user_merge_branch message', :freeze_time do
+ first_request =
+ Gitaly::UserMergeBranchRequest.new(
+ repository: repository.gitaly_repository,
+ user: gitaly_user,
+ commit_id: source_sha,
+ branch: target_branch,
+ expected_old_oid: target_sha,
+ message: message,
+ timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i)
+ )
+
+ second_request = Gitaly::UserMergeBranchRequest.new(apply: true)
+
+ expect_next_instance_of(Gitlab::GitalyClient::QueueEnumerator) do |instance|
+ expect(instance).to receive(:push).with(first_request).and_call_original
+ expect(instance).to receive(:push).with(second_request).and_call_original
+ expect(instance).to receive(:close)
+ end
- it 'sends a user_merge_branch message' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
expect(subject.newrev).to be_present
expect(subject.repo_created).to be(false)
@@ -461,12 +458,14 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
describe '#user_ff_branch' do
let(:target_branch) { 'my-branch' }
+ let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:request) do
Gitaly::UserFFBranchRequest.new(
repository: repository.gitaly_repository,
branch: target_branch,
commit_id: source_sha,
+ expected_old_oid: target_sha,
user: gitaly_user
)
end
@@ -487,7 +486,13 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
.and_return(response)
end
- subject { client.user_ff_branch(user, source_sha, target_branch) }
+ subject do
+ client.user_ff_branch(user,
+ source_sha: source_sha,
+ target_branch: target_branch,
+ target_sha: target_sha
+ )
+ end
it 'sends a user_ff_branch message and returns a BranchUpdate object' do
expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate)
@@ -574,16 +579,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
)
end
- context 'when errors are not raised but returned in the response' do
- before do
- expect_any_instance_of(Gitaly::OperationService::Stub)
- .to receive(:user_cherry_pick).with(kind_of(Gitaly::UserCherryPickRequest), kind_of(Hash))
- .and_return(response)
- end
-
- it_behaves_like 'cherry pick and revert errors'
- end
-
context 'when AccessCheckError is raised' do
let(:raised_error) do
new_detailed_error(
@@ -1149,18 +1144,6 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
end
end
- context 'with pre-receive error' do
- before do
- expect_any_instance_of(Gitaly::OperationService::Stub)
- .to receive(:user_create_tag)
- .and_return(Gitaly::UserCreateTagResponse.new(pre_receive_error: "GitLab: something failed"))
- end
-
- it 'raises a PreReceiveError' do
- expect { add_tag }.to raise_error(Gitlab::Git::PreReceiveError, "something failed")
- end
- end
-
context 'with internal error' do
before do
expect_any_instance_of(Gitaly::OperationService::Stub)
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 14d5cef103b..09d8ea3cc0a 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GitalyClient::RefService do
+RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
let_it_be(:project) { create(:project, :repository, create_tag: 'test') }
let(:storage_name) { project.repository_storage }
@@ -390,10 +390,15 @@ RSpec.describe Gitlab::GitalyClient::RefService do
end
describe '#list_refs' do
+ let(:oid) { project.repository.commit.id }
+
it 'sends a list_refs message' do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:list_refs)
- .with(gitaly_request_with_params(patterns: ['refs/heads/']), kind_of(Hash))
+ .with(
+ gitaly_request_with_params(patterns: ['refs/heads/'], pointing_at_oids: [], peel_tags: false),
+ kind_of(Hash)
+ )
.and_call_original
client.list_refs
@@ -407,6 +412,24 @@ RSpec.describe Gitlab::GitalyClient::RefService do
client.list_refs([Gitlab::Git::TAG_REF_PREFIX])
end
+
+ it 'accepts a pointing_at_oids argument' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:list_refs)
+ .with(gitaly_request_with_params(pointing_at_oids: [oid]), kind_of(Hash))
+ .and_call_original
+
+ client.list_refs(pointing_at_oids: [oid])
+ end
+
+ it 'accepts a peel_tags argument' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:list_refs)
+ .with(gitaly_request_with_params(peel_tags: true), kind_of(Hash))
+ .and_call_original
+
+ client.list_refs(peel_tags: true)
+ end
end
describe '#find_refs_by_oid' do
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 5eb60d2caa5..434550186c1 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::RepositoryService do
using RSpec::Parameterized::TableSyntax
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :repository) }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
let(:client) { described_class.new(project.repository) }
@@ -22,13 +22,38 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
describe '#optimize_repository' do
- it 'sends a optimize_repository message' do
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:optimize_repository)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_return(double(:optimize_repository))
+ shared_examples 'a repository optimization' do
+ it 'sends a optimize_repository message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:optimize_repository)
+ .with(gitaly_request_with_params(
+ strategy: expected_strategy
+ ), kind_of(Hash))
+ .and_call_original
+
+ client.optimize_repository(**params)
+ end
+ end
+
+ context 'with default parameter' do
+ let(:params) { {} }
+ let(:expected_strategy) { :STRATEGY_HEURISTICAL }
+
+ it_behaves_like 'a repository optimization'
+ end
+
+ context 'with heuristical housekeeping strategy' do
+ let(:params) { { eager: false } }
+ let(:expected_strategy) { :STRATEGY_HEURISTICAL }
+
+ it_behaves_like 'a repository optimization'
+ end
+
+ context 'with eager housekeeping strategy' do
+ let(:params) { { eager: true } }
+ let(:expected_strategy) { :STRATEGY_EAGER }
- client.optimize_repository
+ it_behaves_like 'a repository optimization'
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index f5e75242f40..0073d2ebe80 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -155,12 +155,42 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
expect(described_class.stub_creds('default')).to eq(:this_channel_is_insecure)
end
+ it 'returns :this_channel_is_insecure if dns' do
+ address = 'dns:///localhost:9876'
+ stub_repos_storages address
+
+ expect(described_class.stub_creds('default')).to eq(:this_channel_is_insecure)
+ end
+
+ it 'returns :this_channel_is_insecure if dns (short-form)' do
+ address = 'dns:localhost:9876'
+ stub_repos_storages address
+
+ expect(described_class.stub_creds('default')).to eq(:this_channel_is_insecure)
+ end
+
+ it 'returns :this_channel_is_insecure if dns (with authority)' do
+ address = 'dns://1.1.1.1/localhost:9876'
+ stub_repos_storages address
+
+ expect(described_class.stub_creds('default')).to eq(:this_channel_is_insecure)
+ end
+
it 'returns Credentials object if tls' do
address = 'tls://localhost:9876'
stub_repos_storages address
expect(described_class.stub_creds('default')).to be_a(GRPC::Core::ChannelCredentials)
end
+
+ it 'raise an exception if the scheme is not supported' do
+ address = 'custom://localhost:9876'
+ stub_repos_storages address
+
+ expect do
+ described_class.stub_creds('default')
+ end.to raise_error(/unsupported Gitaly address/i)
+ end
end
describe '.create_channel' do
@@ -168,7 +198,10 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
[
['default', 'unix:tmp/gitaly.sock', 'unix:tmp/gitaly.sock'],
['default', 'tcp://localhost:9876', 'localhost:9876'],
- ['default', 'tls://localhost:9876', 'localhost:9876']
+ ['default', 'tls://localhost:9876', 'localhost:9876'],
+ ['default', 'dns:///localhost:9876', 'dns:///localhost:9876'],
+ ['default', 'dns:localhost:9876', 'dns:localhost:9876'],
+ ['default', 'dns://1.1.1.1/localhost:9876', 'dns://1.1.1.1/localhost:9876']
]
end
@@ -289,6 +322,43 @@ RSpec.describe Gitlab::GitalyClient, feature_category: :gitaly do
expect(stub_commit).to have_same_channel(stub_blob)
end
end
+
+ context 'when passed a DNS address' do
+ let(:address) { 'dns:///localhost:9876' }
+
+ before do
+ stub_repos_storages address
+ end
+
+ it 'strips dns:/// prefix before passing it to GRPC::Core::Channel initializer' do
+ expect(Gitaly::CommitService::Stub).to receive(:new).with(
+ address, nil, channel_override: be_a(GRPC::Core::Channel), interceptors: []
+ )
+
+ described_class.stub(:commit_service, 'default')
+ end
+
+ it 'shares the same channel object with other stub' do
+ stub_commit = described_class.stub(:commit_service, 'default')
+ stub_blob = described_class.stub(:blob_service, 'default')
+
+ expect(stub_commit).to have_same_channel(stub_blob)
+ end
+ end
+
+ context 'when passed an unsupported scheme' do
+ let(:address) { 'custom://localhost:9876' }
+
+ before do
+ stub_repos_storages address
+ end
+
+ it 'strips dns:/// prefix before passing it to GRPC::Core::Channel initializer' do
+ expect do
+ described_class.stub(:commit_service, 'default')
+ end.to raise_error(/Unsupported Gitaly address/i)
+ end
+ end
end
describe '.can_use_disk?' do
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index d69bc4d60ee..e93d585bc3c 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Client, feature_category: :importer do
+RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
subject(:client) { described_class.new('foo', parallel: parallel) }
let(:parallel) { true }
diff --git a/spec/lib/gitlab/github_import/clients/proxy_spec.rb b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
index 9fef57f2a38..0baff7bafcb 100644
--- a/spec/lib/gitlab/github_import/clients/proxy_spec.rb
+++ b/spec/lib/gitlab/github_import/clients/proxy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: :import do
+RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category: :importers do
subject(:client) { described_class.new(access_token, client_options) }
let(:access_token) { 'test_token' }
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
index a8dd6b4725d..945b742b025 100644
--- a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter, feature_category: :importers do
let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
let(:client) { double(:client) }
@@ -89,7 +89,7 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each diff note in parallel' do
importer = described_class.new(project, client)
@@ -97,10 +97,8 @@ RSpec.describe Gitlab::GithubImport::Importer::DiffNotesImporter do
.to receive(:each_object_to_import)
.and_yield(github_comment)
- expect(Gitlab::GithubImport::ImportDiffNoteWorker).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::ImportDiffNoteWorker).to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
index 2c1af4f8948..04b694dc0cb 100644
--- a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do
+RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client, parallel: parallel) }
let(:project) { instance_double(Project, id: 4, import_source: 'foo/bar') }
@@ -73,14 +73,12 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each note in parallel' do
allow(importer).to receive(:each_object_to_import).and_yield(issue_event)
- expect(Gitlab::GithubImport::ImportIssueEventWorker).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::ImportIssueEventWorker).to receive(:perform_in).with(
+ 1.second, project.id, an_instance_of(Hash), an_instance_of(String)
)
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
index 4a5525c250e..d6fd1a4739c 100644
--- a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter, feature_category: :importers do
let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
let(:client) { double(:client) }
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each issue in parallel' do
importer = described_class.new(project, client)
@@ -91,12 +91,8 @@ RSpec.describe Gitlab::GithubImport::Importer::IssuesImporter do
.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
- )
+ .to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index 678aa705b6c..fab9d26532d 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
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
+RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter, feature_category: :importers do
let_it_be(:project) { create(:project, :import_started) }
let(:client) { double(:client) }
@@ -110,7 +110,7 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each lfs object in parallel' do
importer = described_class.new(project, client)
@@ -118,10 +118,8 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
expect(service).to receive(:each_list_item).and_yield(lfs_download_object)
end
- expect(Gitlab::GithubImport::ImportLfsObjectWorker).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::ImportLfsObjectWorker).to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
index ca4560b6a1a..841cc8178ea 100644
--- a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::NotesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::NotesImporter, feature_category: :importers do
let(:project) { double(:project, id: 4, import_source: 'foo/bar') }
let(:client) { double(:client) }
@@ -75,7 +75,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NotesImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each note in parallel' do
importer = described_class.new(project, client)
@@ -83,10 +83,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NotesImporter do
.to receive(:each_object_to_import)
.and_yield(github_comment)
- expect(Gitlab::GithubImport::ImportNoteWorker).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::ImportNoteWorker).to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
index 8809d58a252..6a8b14a2690 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
+RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter, feature_category: :importers do
subject(:importer) { described_class.new(project, client, parallel: parallel) }
let(:project) { instance_double('Project', id: 4, import_source: 'foo/bar') }
@@ -143,13 +143,9 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
it 'imports each protected branch in parallel' do
expect(Gitlab::GithubImport::ImportProtectedBranchWorker)
- .to receive(:bulk_perform_in)
- .with(
- 1.second,
- [[project.id, an_instance_of(Hash), an_instance_of(String)]],
- batch_delay: 1.minute,
- batch_size: 1000
- )
+ .to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
+
expect(Gitlab::GithubImport::ObjectCounter)
.to receive(:increment).with(project, :protected_branch, :fetched)
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
index 2e1a3c496cc..3e62e8f473c 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
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter,
+ :clean_gitlab_redis_cache, feature_category: :importers do
using RSpec::Parameterized::TableSyntax
let_it_be(:merge_request) { create(:merge_request) }
@@ -39,6 +40,19 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
expect(merge_request.reviewers).to contain_exactly(author)
end
end
+
+ context 'when because of concurrency an attempt of duplication appeared' do
+ before do
+ allow(MergeRequestReviewer)
+ .to receive(:create!).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'does not change Merge Request reviewers', :aggregate_failures do
+ expect { subject.execute }.not_to change(MergeRequestReviewer, :count)
+
+ expect(merge_request.reviewers).to contain_exactly(author)
+ end
+ end
end
shared_examples 'imports an approval for the Merge Request' do
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 3bf1976ee10..536983fea06 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
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter, :clean_gitlab_redis_cache,
+ feature_category: :importers do
subject(:importer) { described_class.new(project, client) }
let_it_be(:project) { create(:project, import_source: 'foo') }
@@ -107,12 +108,10 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it 'schedule import for each merge request reviewers' do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:bulk_perform_in).with(
- 1.second,
- match_array(expected_worker_payload),
- batch_size: 1000,
- batch_delay: 1.minute
- )
+ .to receive(:perform_in).with(1.second, *expected_worker_payload.first)
+
+ expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
+ .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
importer.parallel_import
end
@@ -127,12 +126,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImpor
it "doesn't schedule import this merge request reviewers" do
expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
- .to receive(:bulk_perform_in).with(
- 1.second,
- expected_worker_payload.slice(1, 1),
- batch_size: 1000,
- batch_delay: 1.minute
- )
+ .to receive(:perform_in).with(1.second, *expected_worker_payload.second)
importer.parallel_import
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index aa92abdb110..eddde272d2c 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
+RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_category: :importers do
let(:url) { 'https://github.com/foo/bar.git' }
let(:project) { create(:project, import_source: 'foo/bar', import_url: url) }
let(:client) { double(:client) }
@@ -92,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
it 'imports each note in parallel' do
importer = described_class.new(project, client)
@@ -101,13 +101,8 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
.and_yield(pull_request)
expect(Gitlab::GithubImport::ImportPullRequestWorker)
- .to receive(:bulk_perform_in)
- .with(
- 1.second,
- [[project.id, an_instance_of(Hash), an_instance_of(String)]],
- batch_delay: 1.minute,
- batch_size: 200
- )
+ .to receive(:perform_in)
+ .with(1.second, project.id, an_instance_of(Hash), an_instance_of(String))
waiter = importer.parallel_import
diff --git a/spec/lib/gitlab/github_import/object_counter_spec.rb b/spec/lib/gitlab/github_import/object_counter_spec.rb
index e522f74416c..92a979eddd2 100644
--- a/spec/lib/gitlab/github_import/object_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/object_counter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do
- let_it_be(:project) { create(:project, :import_started, import_type: 'github') }
+ let_it_be(:project) { create(:project, :import_started, import_type: 'github', import_url: 'https://github.com/vim/vim.git') }
it 'validates the operation being incremented' do
expect { described_class.increment(project, :issue, :unknown) }
@@ -57,4 +57,57 @@ RSpec.describe Gitlab::GithubImport::ObjectCounter, :clean_gitlab_redis_cache do
described_class.increment(project, :issue, :fetched)
end
+
+ describe '.summary' do
+ context 'when there are cached import statistics' do
+ before do
+ described_class.increment(project, :issue, :fetched, value: 10)
+ described_class.increment(project, :issue, :imported, value: 8)
+ end
+
+ it 'includes cached object counts stats in response' do
+ expect(described_class.summary(project)).to eq(
+ 'fetched' => { 'issue' => 10 },
+ 'imported' => { 'issue' => 8 }
+ )
+ end
+ end
+
+ context 'when there are no cached import statistics' do
+ context 'when project import is in progress' do
+ it 'includes an empty object counts stats in response' do
+ expect(described_class.summary(project)).to eq(described_class::EMPTY_SUMMARY)
+ end
+ end
+
+ context 'when project import is not in progress' do
+ let(:checksums) do
+ {
+ 'fetched' => {
+ "issue" => 2,
+ "label" => 10,
+ "note" => 2,
+ "protected_branch" => 2,
+ "pull_request" => 2
+ },
+ "imported" => {
+ "issue" => 2,
+ "label" => 10,
+ "note" => 2,
+ "protected_branch" => 2,
+ "pull_request" => 2
+ }
+ }
+ end
+
+ before do
+ project.import_state.update_columns(checksums: checksums, status: :finished)
+ end
+
+ it 'includes project import checksums in response' do
+ expect(described_class.summary(project)).to eq(checksums)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index cefad3baa31..c351ead91eb 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::ParallelScheduling do
+RSpec.describe Gitlab::GithubImport::ParallelScheduling, feature_category: :importers do
let(:importer_class) do
Class.new do
def self.name
@@ -266,7 +266,7 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
end
end
- describe '#parallel_import' do
+ describe '#parallel_import', :clean_gitlab_redis_cache do
let(:importer) { importer_class.new(project, client) }
let(:repr_class) { double(:representation) }
let(:worker_class) { double(:worker) }
@@ -286,25 +286,82 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do
allow(repr_class)
.to receive(:from_api_response)
.with(object, {})
- .and_return({ title: 'Foo' })
+ .and_return({ title: 'One' }, { title: 'Two' }, { title: 'Three' })
end
context 'with multiple objects' do
before do
+ stub_feature_flags(improved_spread_parallel_import: false)
+
expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
end
it 'imports data in parallel batches with delays' do
expect(worker_class).to receive(:bulk_perform_in)
.with(1.second, [
- [project.id, { title: 'Foo' }, an_instance_of(String)],
- [project.id, { title: 'Foo' }, an_instance_of(String)],
- [project.id, { title: 'Foo' }, an_instance_of(String)]
+ [project.id, { title: 'One' }, an_instance_of(String)],
+ [project.id, { title: 'Two' }, an_instance_of(String)],
+ [project.id, { title: 'Three' }, an_instance_of(String)]
], batch_size: batch_size, batch_delay: batch_delay)
importer.parallel_import
end
end
+
+ context 'when the feature flag `improved_spread_parallel_import` is enabled' do
+ before do
+ stub_feature_flags(improved_spread_parallel_import: true)
+ end
+
+ it 'imports data in parallel with delays respecting parallel_import_batch definition and return job waiter' do
+ allow(::Gitlab::JobWaiter).to receive(:generate_key).and_return('waiter-key')
+ allow(importer).to receive(:parallel_import_batch).and_return({ size: 2, delay: 1.minute })
+
+ expect(importer).to receive(:each_object_to_import)
+ .and_yield(object).and_yield(object).and_yield(object)
+ expect(worker_class).to receive(:perform_in)
+ .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(1.minute + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+
+ job_waiter = importer.parallel_import
+
+ expect(job_waiter.key).to eq('waiter-key')
+ expect(job_waiter.jobs_remaining).to eq(3)
+ end
+
+ context 'when job restarts due to API rate limit or Sidekiq interruption' do
+ before do
+ cache_key = format(described_class::JOB_WAITER_CACHE_KEY,
+ project: project.id, collection: importer.collection_method)
+ Gitlab::Cache::Import::Caching.write(cache_key, 'waiter-key')
+
+ cache_key = format(described_class::JOB_WAITER_REMAINING_CACHE_KEY,
+ project: project.id, collection: importer.collection_method)
+ Gitlab::Cache::Import::Caching.write(cache_key, 3)
+ end
+
+ it "restores job waiter's key and jobs_remaining" do
+ allow(importer).to receive(:parallel_import_batch).and_return({ size: 1, delay: 1.minute })
+
+ expect(importer).to receive(:each_object_to_import).and_yield(object).and_yield(object).and_yield(object)
+
+ expect(worker_class).to receive(:perform_in)
+ .with(1.second, project.id, { title: 'One' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(1.minute + 1.second, project.id, { title: 'Two' }, 'waiter-key').ordered
+ expect(worker_class).to receive(:perform_in)
+ .with(2.minutes + 1.second, project.id, { title: 'Three' }, 'waiter-key').ordered
+
+ job_waiter = importer.parallel_import
+
+ expect(job_waiter.key).to eq('waiter-key')
+ expect(job_waiter.jobs_remaining).to eq(6)
+ end
+ end
+ end
end
describe '#each_object_to_import' do
diff --git a/spec/lib/gitlab/graphql/deprecation_spec.rb b/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb
index c9b47219198..55650b0480e 100644
--- a/spec/lib/gitlab/graphql/deprecation_spec.rb
+++ b/spec/lib/gitlab/graphql/deprecations/deprecation_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
require 'active_model'
-RSpec.describe ::Gitlab::Graphql::Deprecation do
+RSpec.describe ::Gitlab::Graphql::Deprecations::Deprecation, feature_category: :integrations do
let(:options) { {} }
subject(:deprecation) { described_class.new(**options) }
diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb
index 974951ab30c..9e754f9673e 100644
--- a/spec/lib/gitlab/graphql/markdown_field_spec.rb
+++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Graphql::MarkdownField do
field = class_with_markdown_field(:test_html, null: true, method: :hello).fields['testHtml']
expect(field.name).to eq('testHtml')
- expect(field.description).to eq('The GitLab Flavored Markdown rendering of `hello`')
+ expect(field.description).to eq('GitLab Flavored Markdown rendering of `hello`')
expect(field.type).to eq(GraphQL::Types::String)
expect(field.complexity).to eq(5)
end
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
index 2c2ec821385..c556f507876 100644
--- a/spec/lib/gitlab/graphql/queries_spec.rb
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -360,5 +360,29 @@ RSpec.describe Gitlab::Graphql::Queries do
end
end
end
+
+ context 'a query containing a persist directive' do
+ let(:path) { 'persist_directive.query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'is tagged as a client query' do
+ expect(subject.validate(schema).first).to eq :client_query
+ end
+ end
+
+ context 'a query containing a persistantly directive' do
+ let(:path) { 'persistantly_directive.query.graphql' }
+
+ it 'is not tagged as a client query' do
+ expect(subject.validate(schema).first).not_to eq :client_query
+ end
+ end
+
+ context 'a query containing a persist field' do
+ let(:path) { 'persist_field.query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
end
end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
index 5e2c6be8993..5137e098e2d 100644
--- a/spec/lib/gitlab/http_connection_adapter_spec.rb
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -111,20 +111,6 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
end
end
- context 'when http(s) environment variable is set' do
- before do
- stub_env('https_proxy' => 'https://my.proxy')
- end
-
- it 'sets up the connection' do
- expect(connection).to be_a(Gitlab::NetHttpAdapter)
- expect(connection.address).to eq('example.org')
- expect(connection.hostname_override).to eq(nil)
- expect(connection.addr_port).to eq('example.org')
- expect(connection.port).to eq(443)
- end
- end
-
context 'when URL scheme is not HTTP/HTTPS' do
let(:uri) { URI('ssh://example.org') }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8750bf4387c..0c2c3ffc664 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -386,6 +386,7 @@ create_access_levels:
- user
- protected_tag
- group
+- deploy_key
container_repositories:
- project
- name
@@ -486,6 +487,7 @@ project:
- requesters
- namespace_members
- namespace_requesters
+- namespace_members_and_requesters
- deploy_keys_projects
- deploy_keys
- users_star_projects
@@ -669,6 +671,8 @@ project:
- disable_download_button
- dependency_list_exports
- sbom_occurrences
+- analytics_dashboards_configuration_project
+- analytics_dashboards_pointer
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 7e17d56def0..572f809e43b 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Import/Export attribute configuration' do
it 'has no new columns' do
relation_names_for(:project).each do |relation_name|
relation_class = relation_class_for_name(relation_name)
- relation_attributes = relation_class.new.attributes.keys - relation_class.encrypted_attributes.keys.map(&:to_s)
+ relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
diff --git a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb b/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
index 18f2e8f80d7..6c997dc1361 100644
--- a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
@@ -13,7 +13,7 @@ require 'spec_helper'
# - randomly generated fields like tokens
#
# as these are expected to change between import/export cycles.
-RSpec.describe Gitlab::ImportExport do
+RSpec.describe Gitlab::ImportExport, feature_category: :importers do
include ImportExport::CommonUtil
include ConfigurationHelper
include ImportExport::ProjectTreeExpectations
@@ -25,7 +25,8 @@ RSpec.describe Gitlab::ImportExport do
end
it 'yields the initial tree when importing and exporting it again' do
- project = create(:project, creator: create(:user, :admin))
+ project = create(:project)
+ user = create(:user, :admin)
# We first generate a test fixture dynamically from a seed-fixture, so as to
# account for any fields in the initial fixture that are missing and set to
@@ -34,6 +35,7 @@ RSpec.describe Gitlab::ImportExport do
expect(
restore_then_save_project(
project,
+ user,
import_path: seed_fixture_path,
export_path: test_fixture_path)
).to be true
@@ -42,6 +44,7 @@ RSpec.describe Gitlab::ImportExport do
expect(
restore_then_save_project(
project,
+ user,
import_path: test_fixture_path,
export_path: test_tmp_path)
).to be true
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index c9d559c992c..53d205850c8 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
- let(:project) { create(:project) }
let(:import_file) { fixture_file_upload('spec/features/projects/import_export/test_project_export.tar.gz') }
+ let_it_be(:project) { create(:project) }
subject(:importer) { described_class.new(project) }
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index 02ac8065c9f..103d3512e8b 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
+RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer, feature_category: :importers do
let_it_be(:user) { create(:user) }
let_it_be(:release) { create(:release) }
let_it_be(:group) { create(:group) }
@@ -213,59 +213,143 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
end
end
- describe 'conditional export of included associations' do
+ describe 'with inaccessible associations' do
+ let_it_be(:milestone) { create(:milestone, project: exportable) }
+ let_it_be(:issue) { create(:issue, assignees: [user], project: exportable, milestone: milestone) }
+ let_it_be(:label1) { create(:label, project: exportable) }
+ let_it_be(:label2) { create(:label, project: exportable) }
+ let_it_be(:link1) { create(:label_link, label: label1, target: issue) }
+ let_it_be(:link2) { create(:label_link, label: label2, target: issue) }
+
+ let(:options) { { include: [{ label_links: { include: [:label] } }, { milestone: { include: [] } }] } }
+
let(:include) do
- [{ issues: { include: [{ label_links: { include: [:label] } }] } }]
+ [{ issues: options }]
end
- let(:include_if_exportable) do
- { issues: [:label_links] }
+ shared_examples 'record with exportable associations' do
+ it 'includes exportable association' do
+ expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
+
+ subject.execute
+ end
end
- let_it_be(:label) { create(:label, project: exportable) }
- let_it_be(:link) { create(:label_link, label: label, target: issue) }
+ context 'conditional export of included associations' do
+ let(:include_if_exportable) do
+ { issues: [:label_links, :milestone] }
+ end
- context 'when association is exportable' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ context 'when association is exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(true)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json(options) }
end
end
- it 'includes exportable association' do
- expected_issue = issue.to_json(include: [{ label_links: { include: [:label] } }])
+ context 'when an association is not exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(true)
+ allow(issue).to receive(:exportable_association?).with(:milestone, current_user: user).and_return(false)
+ end
+ end
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(expected_issue))
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json(include: [{ label_links: { include: [:label] } }]) }
+ end
+ end
- subject.execute
+ context 'when association does not respond to exportable_association?' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:respond_to?).and_call_original
+ allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) { issue.to_json }
+ end
end
end
- context 'when association is not exportable' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:exportable_association?).with(:label_links, current_user: user).and_return(false)
+ context 'export of included restricted associations' do
+ let(:many_relation) { :label_links }
+ let(:single_relation) { :milestone }
+ let(:issue_hash) { issue.as_json(options).with_indifferent_access }
+ let(:expected_issue) { issue.to_json(options) }
+
+ context 'when the association is restricted' do
+ context 'when some association records are exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
+ allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1])
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) do
+ issue_hash[many_relation].delete_if { |record| record['id'] == link2.id }
+ issue_hash.to_json(options)
+ end
+ end
end
- end
- it 'filters out not exportable association' do
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+ context 'when all association records are exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([many_relation])
+ allow(issue).to receive(:readable_records).with(many_relation, current_user: user).and_return([link1, link2])
+ end
+ end
- subject.execute
- end
- end
+ it_behaves_like 'record with exportable associations'
+ end
- context 'when association does not respond to exportable_association?' do
- before do
- allow_next_found_instance_of(Issue) do |issue|
- allow(issue).to receive(:respond_to?).with(:exportable_association?).and_return(false)
+ context 'when the single association record is exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
+ allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(milestone)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations'
+ end
+
+ context 'when the single association record is not exportable' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([single_relation])
+ allow(issue).to receive(:readable_records).with(single_relation, current_user: user).and_return(nil)
+ end
+ end
+
+ it_behaves_like 'record with exportable associations' do
+ let(:expected_issue) do
+ issue_hash[single_relation] = nil
+ issue_hash.to_json(options)
+ end
+ end
end
end
- it 'filters out not exportable association' do
- expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, array_including(issue.to_json))
+ context 'when the associations are not restricted' do
+ before do
+ allow_next_found_instance_of(Issue) do |issue|
+ allow(issue).to receive(:restricted_associations).with([many_relation, single_relation]).and_return([])
+ end
+ end
- subject.execute
+ it_behaves_like 'record with exportable associations'
end
end
end
diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
index 936c63fd6cd..d133f54ade5 100644
--- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_memory_store_caching do
+RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_memory_store_caching, feature_category: :importers do
let(:group) { create(:group).tap { |g| g.add_maintainer(importer_user) } }
let(:project) { create(:project, :repository, group: group) }
let(:members_mapper) { double('members_mapper').as_null_object }
@@ -418,21 +418,73 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
end
end
- context 'merge request access level object' do
- let(:relation_sym) { :'ProtectedBranch::MergeAccessLevel' }
- let(:relation_hash) { { 'access_level' => 30, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } }
+ describe 'protected branch access levels' do
+ shared_examples 'access levels' do
+ let(:relation_hash) { { 'access_level' => access_level, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } }
- it 'sets access level to maintainer' do
- expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ context 'when access level is no one' do
+ let(:access_level) { Gitlab::Access::NO_ACCESS }
+
+ it 'keeps no one access level' do
+ expect(created_object.access_level).to equal(access_level)
+ end
+ end
+
+ context 'when access level is below maintainer' do
+ let(:access_level) { Gitlab::Access::DEVELOPER }
+
+ it 'sets access level to maintainer' do
+ expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ end
+ end
+
+ context 'when access level is above maintainer' do
+ let(:access_level) { Gitlab::Access::OWNER }
+
+ it 'sets access level to maintainer' do
+ expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ end
+ end
+
+ describe 'root ancestor membership' do
+ let(:access_level) { Gitlab::Access::DEVELOPER }
+
+ context 'when importer user is root group owner' do
+ let(:importer_user) { create(:user) }
+
+ it 'keeps access level as is' do
+ group.add_owner(importer_user)
+
+ expect(created_object.access_level).to equal(access_level)
+ end
+ end
+
+ context 'when user membership in root group is missing' do
+ it 'sets access level to maintainer' do
+ group.members.delete_all
+
+ expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ end
+ end
+
+ context 'when root ancestor is not a group' do
+ it 'sets access level to maintainer' do
+ expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ end
+ end
+ end
+ end
+
+ describe 'merge access level' do
+ let(:relation_sym) { :'ProtectedBranch::MergeAccessLevel' }
+
+ include_examples 'access levels'
end
- end
- context 'push access level object' do
- let(:relation_sym) { :'ProtectedBranch::PushAccessLevel' }
- let(:relation_hash) { { 'access_level' => 30, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } }
+ describe 'push access level' do
+ let(:relation_sym) { :'ProtectedBranch::PushAccessLevel' }
- it 'sets access level to maintainer' do
- expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER)
+ include_examples 'access levels'
end
end
end
diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
index ac646087a95..6053df8ba97 100644
--- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
@@ -9,7 +9,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer do
+RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_category: :importers do
let_it_be(:importable, reload: true) do
create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
end
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 2699dc10b18..125d1736b9b 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -6,7 +6,7 @@ def match_mr1_note(content_regex)
MergeRequest.find_by(title: 'MR1').notes.find { |n| n.note.match(/#{content_regex}/) }
end
-RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
+RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :importers do
include ImportExport::CommonUtil
using RSpec::Parameterized::TableSyntax
diff --git a/spec/lib/gitlab/import_export/references_configuration_spec.rb b/spec/lib/gitlab/import_export/references_configuration_spec.rb
index 6320fbed975..ad165790b77 100644
--- a/spec/lib/gitlab/import_export/references_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/references_configuration_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Import/Export Project configuration' do
context "where relation #{params[:relation_path]}" do
it 'does not have prohibited keys' do
relation_class = relation_class_for_name(relation_name)
- relation_attributes = relation_class.new.attributes.keys - relation_class.encrypted_attributes.keys.map(&:to_s)
+ relation_attributes = relation_class.new.attributes.keys - relation_class.attr_encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
prohibited_keys = current_attributes.select do |attribute|
prohibited_key?(attribute) || !relation_class.attribute_method?(attribute)
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index b3730d85f13..dd81b8b846d 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::VersionChecker, feature_category: :import do
+RSpec.describe Gitlab::ImportExport::VersionChecker, feature_category: :importers do
include ImportExport::CommonUtil
let!(:shared) { Gitlab::ImportExport::Shared.new(nil) }
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 e2c67c68eb7..7573741082b 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
@@ -28,10 +28,10 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
}
end
- subject(:to_s) { described_class.new(incident_payload).to_s }
+ subject(:description) { described_class.new(incident_payload).to_s }
it 'returns description' do
- expect(to_s).to eq(
+ expect(description).to eq(
<<~MARKDOWN.chomp
**Incident:** [My new incident](https://webdemo.pagerduty.com/incidents/PRORDTY)#{markdown_line_break}
**Incident number:** 33#{markdown_line_break}
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
freeze_time do
now = Time.current.utc.strftime('%d %B %Y, %-l:%M%p (%Z)')
- expect(to_s).to include(
+ expect(description).to include(
<<~MARKDOWN.chomp
**Created at:** #{now}#{markdown_line_break}
MARKDOWN
@@ -70,7 +70,7 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
end
it 'assignees is a list of links' do
- expect(to_s).to include(
+ expect(description).to include(
<<~MARKDOWN.chomp
**Assignees:** [Laura Haley](https://laura.pagerduty.com), [John Doe](https://john.pagerduty.com)#{markdown_line_break}
MARKDOWN
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription d
end
it 'impacted service is a single link' do
- expect(to_s).to include(
+ expect(description).to include(
<<~MARKDOWN.chomp
**Impacted service:** [XDB Cluster](https://xdb.pagerduty.com)
MARKDOWN
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index ce67d1d0297..8a88328e0c1 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -5,7 +5,7 @@ require 'rspec-parameterized'
require 'support/helpers/rails_helpers'
RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
- feature_category: :scalability do
+ :use_null_store_as_repository_cache, feature_category: :scalability do
using RSpec::Parameterized::TableSyntax
describe '.add_instrumentation_data', :request_store do
@@ -23,42 +23,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
expect(payload).to include(db_count: 0, db_cached_count: 0, db_write_count: 0)
end
- shared_examples 'make Gitaly calls' do
- context 'when Gitaly calls are made' do
- it 'adds Gitaly and Redis data' do
- project = create(:project)
- RequestStore.clear!
- project.repository.exists?
+ context 'when Gitaly calls are made' do
+ it 'adds Gitaly and Redis data' do
+ project = create(:project)
+ RequestStore.clear!
+ project.repository.exists?
- subject
-
- expect(payload[:gitaly_calls]).to eq(1)
- expect(payload[:gitaly_duration_s]).to be >= 0
- # With MultiStore, the number of `redis_calls` depends on whether primary_store
- # (Gitlab::Redis::Repositorycache) and secondary_store (Gitlab::Redis::Cache) are of the same instance.
- # In GitLab.com CI, primary and secondary are the same instance, thus only 1 call being made. If primary
- # and secondary are different instances, an additional fallback read to secondary_store will be made because
- # the first `get` call is a cache miss. Then, the following expect will fail.
- expect(payload[:redis_calls]).to eq(1)
- expect(payload[:redis_duration_ms]).to be_nil
- end
- end
- end
-
- context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
- end
-
- it_behaves_like 'make Gitaly calls'
- end
+ subject
- context 'when multistore ff use_primary_and_secondary_stores_for_repository_cache is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
+ expect(payload[:gitaly_calls]).to eq(1)
+ expect(payload[:gitaly_duration_s]).to be >= 0
+ expect(payload[:redis_calls]).to eq(nil)
+ expect(payload[:redis_duration_ms]).to be_nil
end
-
- it_behaves_like 'make Gitaly calls'
end
context 'when Redis calls are made' do
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index a9edb2b530b..af2da8f20c0 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::JobWaiter, :redis do
+RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do
describe '.notify' do
it 'pushes the jid to the named queue' do
key = described_class.new.key
@@ -15,6 +15,14 @@ RSpec.describe Gitlab::JobWaiter, :redis do
end
end
+ describe '.generate_key' do
+ it 'generates and return a new key' do
+ key = described_class.generate_key
+
+ expect(key).to include('gitlab:job_waiter:')
+ end
+ end
+
describe '#wait' do
let(:waiter) { described_class.new(2) }
diff --git a/spec/lib/gitlab/logger_spec.rb b/spec/lib/gitlab/logger_spec.rb
index ed22af8355f..9f11f3ac629 100644
--- a/spec/lib/gitlab/logger_spec.rb
+++ b/spec/lib/gitlab/logger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Logger do
+RSpec.describe Gitlab::Logger, feature_category: :logging do
describe '.build' do
before do
allow(described_class).to receive(:file_name_noext).and_return('log')
@@ -25,40 +25,28 @@ RSpec.describe Gitlab::Logger do
using RSpec::Parameterized::TableSyntax
where(:env_value, :resulting_level) do
- 0 | described_class::DEBUG
- :debug | described_class::DEBUG
'debug' | described_class::DEBUG
'DEBUG' | described_class::DEBUG
'DeBuG' | described_class::DEBUG
- 1 | described_class::INFO
- :info | described_class::INFO
'info' | described_class::INFO
'INFO' | described_class::INFO
'InFo' | described_class::INFO
- 2 | described_class::WARN
- :warn | described_class::WARN
'warn' | described_class::WARN
'WARN' | described_class::WARN
'WaRn' | described_class::WARN
- 3 | described_class::ERROR
- :error | described_class::ERROR
'error' | described_class::ERROR
'ERROR' | described_class::ERROR
'ErRoR' | described_class::ERROR
- 4 | described_class::FATAL
- :fatal | described_class::FATAL
'fatal' | described_class::FATAL
'FATAL' | described_class::FATAL
'FaTaL' | described_class::FATAL
- 5 | described_class::UNKNOWN
- :unknown | described_class::UNKNOWN
'unknown' | described_class::UNKNOWN
'UNKNOWN' | described_class::UNKNOWN
'UnKnOwN' | described_class::UNKNOWN
end
with_them do
- it 'builds logger if valid log level' do
+ it 'builds logger if valid log level is provided' do
stub_env('GITLAB_LOG_LEVEL', env_value)
expect(subject.level).to eq(resulting_level)
@@ -69,15 +57,15 @@ RSpec.describe Gitlab::Logger do
describe '.log_level' do
context 'if GITLAB_LOG_LEVEL is set' do
before do
- stub_env('GITLAB_LOG_LEVEL', described_class::ERROR)
+ stub_env('GITLAB_LOG_LEVEL', 'error')
end
- it 'returns value of GITLAB_LOG_LEVEL' do
- expect(described_class.log_level).to eq(described_class::ERROR)
+ it 'returns value defined by GITLAB_LOG_LEVEL' do
+ expect(described_class.log_level).to eq('error')
end
it 'ignores fallback' do
- expect(described_class.log_level(fallback: described_class::FATAL)).to eq(described_class::ERROR)
+ expect(described_class.log_level(fallback: described_class::FATAL)).to eq('error')
end
end
diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb
index 0c2c9b89005..7259b5e2484 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::MailRoom do
+RSpec.describe Gitlab::MailRoom, feature_category: :build do
let(:default_port) { 143 }
let(:log_path) { Rails.root.join('log', 'mail_room_json.log').to_s }
@@ -328,4 +328,123 @@ RSpec.describe Gitlab::MailRoom do
end
end
end
+
+ describe 'mailroom encrypted configuration' do
+ context "when parsing secrets.yml" do
+ let(:application_secrets_file) { Rails.root.join('spec/fixtures/mail_room/secrets.yml.erb').to_s }
+ let(:encrypted_settings_key_base) { '0123456789abcdef' * 8 }
+
+ before do
+ allow(described_class).to receive(:application_secrets_file).and_return(application_secrets_file)
+ stub_env('KEY', 'an environment variable value')
+ described_class.instance_variable_set(:@application_secrets, nil)
+ end
+
+ after do
+ described_class.instance_variable_set(:@application_secrets, nil)
+ end
+
+ it 'reads in the secrets.yml file as erb and merges shared and test environments' do
+ application_secrets = described_class.send(:application_secrets)
+
+ expect(application_secrets).to match(a_hash_including(
+ a_shared_key: 'this key is shared',
+ an_overriden_shared_key: 'the merge overwrote this key',
+ an_environment_specific_key: 'test environment value',
+ erb_env_key: 'an environment variable value',
+ encrypted_settings_key_base: encrypted_settings_key_base
+ ))
+
+ expect(application_secrets[:an_unread_key]).to be_nil
+ end
+ end
+
+ context "when parsing gitlab.yml" do
+ let(:plain_configs) { configs }
+ let(:shared_path_config) do
+ { shared: { path: '/this/is/my/shared_path' } }.merge(configs)
+ end
+
+ let(:encrypted_settings_config) do
+ {
+ shared: { path: '/this/is/my/shared_path' },
+ encrypted_settings: { path: '/this/is/my_custom_encrypted_path' }
+ }.merge(configs)
+ end
+
+ let(:encrypted_file_config) do
+ configs.deep_merge({
+ incoming_email: { encrypted_secret_file: '/custom_incoming_secret.yaml.enc' },
+ service_desk_email: { encrypted_secret_file: '/custom_service_desk_secret.yaml.enc' }
+ })
+ end
+
+ it 'returns default encrypted_secret_file path' do
+ allow(described_class).to receive(:load_yaml).and_return(plain_configs)
+
+ expect(described_class.send(:encrypted_secret_file, :incoming_email))
+ .to end_with('shared/encrypted_settings/incoming_email.yaml.enc')
+
+ expect(described_class.send(:encrypted_secret_file, :service_desk_email))
+ .to end_with('shared/encrypted_settings/service_desk_email.yaml.enc')
+ end
+
+ it 'returns encrypted_secret_file relative to custom shared path' do
+ allow(described_class).to receive(:load_yaml).and_return(shared_path_config)
+
+ expect(described_class.send(:encrypted_secret_file, :incoming_email))
+ .to eq('/this/is/my/shared_path/encrypted_settings/incoming_email.yaml.enc')
+
+ expect(described_class.send(:encrypted_secret_file, :service_desk_email))
+ .to eq('/this/is/my/shared_path/encrypted_settings/service_desk_email.yaml.enc')
+ end
+
+ it 'returns custom encrypted_secret_file' do
+ allow(described_class).to receive(:load_yaml).and_return(encrypted_file_config)
+
+ expect(described_class.send(:encrypted_secret_file, :incoming_email))
+ .to eq('/custom_incoming_secret.yaml.enc')
+
+ expect(described_class.send(:encrypted_secret_file, :service_desk_email))
+ .to eq('/custom_service_desk_secret.yaml.enc')
+ end
+ end
+
+ context 'when using encrypted secrets' do
+ let(:mail_room_template) { ERB.new(File.read(Rails.root.join("./config/mail_room.yml"))).result }
+ let(:mail_room_yml) { YAML.safe_load(mail_room_template, permitted_classes: [Symbol]) }
+ let(:application_secrets) { { encrypted_settings_key_base: '0123456789abcdef' * 8 } } # gitleaks:allow
+ let(:configs) do
+ {
+ encrypted_settings: { path: 'spec/fixtures/mail_room/encrypted_secrets' }
+ }.merge({
+ incoming_email: incoming_email_config,
+ service_desk_email: service_desk_email_config
+ })
+ end
+
+ before do
+ allow(described_class).to receive(:application_secrets).and_return(application_secrets)
+ end
+
+ it 'renders the encrypted secrets into the configuration correctly' do
+ expect(mail_room_yml[:mailboxes]).to be_an(Array)
+ expect(mail_room_yml[:mailboxes].length).to eq(2)
+
+ expect(mail_room_yml[:mailboxes][0]).to match(
+ a_hash_including(
+ password: 'abc123',
+ email: 'incoming-test-account@gitlab.com'
+ )
+ )
+
+ expect(mail_room_yml[:mailboxes][1]).to match(
+ a_hash_including(
+ password: '123abc',
+ email: 'service-desk-test-account@gitlab.example.com'
+ )
+ )
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
index 9242344ead2..4c0f6baef8f 100644
--- a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
describe '#handler' do
context 'when handler is not set' do
it 'defaults to NullHandler' do
- expect(configuration.handler).to be(Gitlab::Memory::Watchdog::NullHandler.instance)
+ expect(configuration.handler).to be(Gitlab::Memory::Watchdog::Handlers::NullHandler.instance)
end
end
end
diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
index a901be84a21..035652abfe6 100644
--- a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'prometheus/client'
-require 'sidekiq'
-require_dependency 'gitlab/cluster/lifecycle_events'
+require 'spec_helper'
-RSpec.describe Gitlab::Memory::Watchdog::Configurator do
+RSpec.describe Gitlab::Memory::Watchdog::Configurator, feature_category: :application_performance do
shared_examples 'as configurator' do |handler_class, event_reporter_class, sleep_time_env, sleep_time|
it 'configures the correct handler' do
configurator.call(configuration)
@@ -92,7 +89,7 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
end
it_behaves_like 'as configurator',
- Gitlab::Memory::Watchdog::PumaHandler,
+ Gitlab::Memory::Watchdog::Handlers::PumaHandler,
Gitlab::Memory::Watchdog::EventReporter,
'GITLAB_MEMWD_SLEEP_TIME_SEC',
described_class::DEFAULT_SLEEP_INTERVAL_S
@@ -200,7 +197,7 @@ RSpec.describe Gitlab::Memory::Watchdog::Configurator do
subject(:configurator) { described_class.configure_for_sidekiq }
it_behaves_like 'as configurator',
- Gitlab::Memory::Watchdog::TermProcessHandler,
+ Gitlab::Memory::Watchdog::Handlers::SidekiqHandler,
Gitlab::Memory::Watchdog::SidekiqEventReporter,
'SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL',
described_class::DEFAULT_SIDEKIQ_SLEEP_INTERVAL_S
diff --git a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
index f667bc724d2..f1d241249e2 100644
--- a/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/event_reporter_spec.rb
@@ -106,11 +106,12 @@ RSpec.describe Gitlab::Memory::Watchdog::EventReporter, feature_category: :appli
it 'logs violation' do
expect(logger).to receive(:warn)
- .with(
+ .with({
pid: Process.pid,
worker_id: 'worker_1',
memwd_rss_bytes: 1024,
- message: 'dummy_text')
+ message: 'dummy_text'
+ })
subject
end
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
new file mode 100644
index 00000000000..09c76de9611
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/handlers/null_handler_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::NullHandler, feature_category: :application_performance do
+ subject(:handler) { described_class.instance }
+
+ describe '#call' do
+ it 'does nothing' do
+ expect(handler.call).to be(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
new file mode 100644
index 00000000000..7df95c1722e
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/handlers/puma_handler_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::PumaHandler, feature_category: :application_performance do
+ # rubocop: disable RSpec/VerifiedDoubles
+ # In tests, the Puma constant is not loaded so we cannot make this an instance_double.
+ let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') }
+ let(:puma_worker_handle) { double('worker') }
+ # rubocop: enable RSpec/VerifiedDoubles
+
+ subject(:handler) { described_class.new({}) }
+
+ 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).to receive(:term)
+
+ expect(handler.call).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
new file mode 100644
index 00000000000..d1f303e7731
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/handlers/sidekiq_handler_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'sidekiq'
+
+RSpec.describe Gitlab::Memory::Watchdog::Handlers::SidekiqHandler, feature_category: :application_performance do
+ let(:sleep_time) { 3 }
+ let(:shutdown_timeout_seconds) { 30 }
+ let(:handler_iterations) { 0 }
+ let(:logger) { instance_double(::Logger) }
+ let(:pid) { $$ }
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time)
+ .and_return(0, 1, shutdown_timeout_seconds, 0, 1, Sidekiq[:timeout] + 2)
+ allow(Process).to receive(:kill)
+ allow(::Sidekiq).to receive(:logger).and_return(logger)
+ allow(logger).to receive(:warn)
+ allow(Process).to receive(:getpgrp).and_return(pid)
+ end
+
+ subject(:handler) do
+ described_class.new(shutdown_timeout_seconds, sleep_time).tap do |instance|
+ # We need to defuse `sleep` and stop the handler after n iteration
+ iterations = 0
+ allow(instance).to receive(:sleep) do
+ if (iterations += 1) > handler_iterations
+ instance.stop
+ end
+ end
+ end
+ end
+
+ describe '#call' do
+ shared_examples_for 'handler issues kill command' do
+ it 'logs sending signal' do
+ logs.each do |log|
+ expect(::Sidekiq.logger).to receive(:warn).once.ordered.with(log)
+ end
+
+ handler.call
+ end
+
+ it 'sends TERM to the current process' do
+ signal_params.each do |args|
+ expect(Process).to receive(:kill).once.ordered.with(*args.first(2))
+ end
+
+ expect(handler.call).to be(true)
+ end
+ end
+
+ def log(signal, pid, explanation, wait_time = nil)
+ {
+ pid: pid,
+ worker_id: ::Prometheus::PidProvider.worker_id,
+ memwd_handler_class: described_class.to_s,
+ memwd_signal: signal,
+ memwd_explanation: explanation,
+ memwd_wait_time: wait_time,
+ message: "Sending signal and waiting"
+ }
+ end
+
+ let(:logs) do
+ signal_params.map { |args| log(*args) }
+ end
+
+ context "when stop is received after TSTP" do
+ let(:signal_params) do
+ [
+ [:TSTP, pid, 'stop fetching new jobs', shutdown_timeout_seconds]
+ ]
+ end
+
+ it_behaves_like 'handler issues kill command'
+ end
+
+ context "when stop is received after TERM" do
+ let(:handler_iterations) { 1 }
+ let(:signal_params) do
+ [
+ [:TSTP, pid, 'stop fetching new jobs', shutdown_timeout_seconds],
+ [:TERM, pid, 'gracefully shut down', Sidekiq[:timeout] + 2]
+ ]
+ end
+
+ it_behaves_like 'handler issues kill command'
+ end
+
+ context "when stop is not received" do
+ let(:handler_iterations) { 2 }
+ let(:gpid) { pid + 1 }
+ let(:kill_pid) { pid }
+ let(:signal_params) do
+ [
+ [:TSTP, pid, 'stop fetching new jobs', shutdown_timeout_seconds],
+ [:TERM, pid, 'gracefully shut down', Sidekiq[:timeout] + 2],
+ [:KILL, kill_pid, 'hard shut down', nil]
+ ]
+ end
+
+ before do
+ allow(Process).to receive(:getpgrp).and_return(gpid)
+ end
+
+ context 'when process is not group leader' do
+ it_behaves_like 'handler issues kill command'
+ end
+
+ context 'when process is a group leader' do
+ let(:gpid) { pid }
+ let(:kill_pid) { 0 }
+
+ it_behaves_like 'handler issues kill command'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index 0b2f24476d9..dd6bfb6da2c 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
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(:handler) { instance_double(described_class::Handlers::NullHandler) }
let(:reporter) { instance_double(described_class::EventReporter) }
let(:sleep_time_seconds) { 60 }
let(:threshold_violated) { false }
@@ -60,10 +60,10 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
it 'reports started event once' do
expect(reporter).to receive(:started).once
- .with(
+ .with({
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: sleep_time_seconds
- )
+ })
watchdog.call
end
@@ -77,11 +77,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
context 'when no monitors are configured' do
it 'reports stopped event once with correct reason' do
expect(reporter).to receive(:stopped).once
- .with(
+ .with({
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: sleep_time_seconds,
memwd_reason: 'monitors are not configured'
- )
+ })
watchdog.call
end
@@ -96,11 +96,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
it 'reports stopped event once' do
expect(reporter).to receive(:stopped).once
- .with(
+ .with({
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: sleep_time_seconds,
memwd_reason: 'background task stopped'
- )
+ })
watchdog.call
end
@@ -149,13 +149,13 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
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"
- )
+ 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
@@ -163,11 +163,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
it 'executes handler and stops the watchdog' do
expect(handler).to receive(:call).and_return(true)
expect(reporter).to receive(:stopped).once
- .with(
+ .with({
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: sleep_time_seconds,
memwd_reason: 'successfully handled'
- )
+ })
watchdog.call
end
@@ -185,7 +185,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
it 'always uses the NullHandler' do
expect(handler).not_to receive(:call)
- expect(described_class::NullHandler.instance).to receive(:call).and_return(true)
+ expect(described_class::Handlers::NullHandler.instance).to receive(:call).and_return(true)
watchdog.call
end
@@ -216,56 +216,4 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, feature_category:
end
end
end
-
- context 'handlers' do
- context 'NullHandler' do
- subject(:handler) { described_class::NullHandler.instance }
-
- describe '#call' do
- it 'does nothing' do
- expect(handler.call).to be(false)
- end
- end
- end
-
- context 'TermProcessHandler' 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)
-
- expect(handler.call).to be(true)
- end
- end
- end
-
- context 'PumaHandler' do
- # rubocop: disable RSpec/VerifiedDoubles
- # In tests, the Puma constant is not loaded so we cannot make this an instance_double.
- let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') }
- let(:puma_worker_handle) { double('worker') }
- # rubocop: enable RSpec/VerifiedDoubles
-
- subject(:handler) { described_class::PumaHandler.new({}) }
-
- 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).to receive(:term)
-
- expect(handler.call).to be(true)
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/metrics/environment_spec.rb b/spec/lib/gitlab/metrics/environment_spec.rb
new file mode 100644
index 00000000000..e94162e625e
--- /dev/null
+++ b/spec/lib/gitlab/metrics/environment_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/helpers/stub_env'
+
+RSpec.describe Gitlab::Metrics::Environment, feature_category: :error_budgets do
+ include StubENV
+
+ describe '.web? .api? .git?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:env_var, :git, :api, :web) do
+ 'web' | false | false | true
+ 'api' | false | true | false
+ 'git' | true | false | false
+ 'websockets' | false | false | false
+ nil | true | true | true
+ '' | true | true | true
+ end
+
+ with_them do
+ it 'each method returns as expected' do
+ stub_env('GITLAB_METRICS_INITIALIZE', env_var)
+
+ expect(described_class.git?).to eq(git)
+ expect(described_class.web?).to eq(web)
+ expect(described_class.api?).to eq(api)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
index 1aa2c4398a7..5248cd08770 100644
--- a/spec/lib/gitlab/metrics/global_search_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
@@ -6,10 +6,22 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
using RSpec::Parameterized::TableSyntax
describe '#initialize_slis!' do
+ let(:api_endpoint_labels) do
+ [a_hash_including(endpoint_id: 'GET /api/:version/search')]
+ end
+
+ let(:web_endpoint_labels) do
+ [a_hash_including(endpoint_id: "SearchController#show")]
+ end
+
+ let(:all_endpoint_labels) do
+ api_endpoint_labels + web_endpoint_labels
+ end
+
it 'initializes Apdex SLIs for global_search' do
expect(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(
:global_search,
- a_kind_of(Array)
+ array_including(all_endpoint_labels)
)
described_class.initialize_slis!
@@ -18,11 +30,60 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
it 'initializes ErrorRate SLIs for global_search' do
expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(
:global_search,
- a_kind_of(Array)
+ array_including(all_endpoint_labels)
)
described_class.initialize_slis!
end
+
+ context "when initializeing for limited types" do
+ where(:api, :web) do
+ [true, false].repeated_permutation(2).to_a
+ end
+
+ with_them do
+ it 'only initializes for the relevant endpoints', :aggregate_failures do
+ allow(Gitlab::Metrics::Environment).to receive(:api?).and_return(api)
+ allow(Gitlab::Metrics::Environment).to receive(:web?).and_return(web)
+ allow(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli)
+ allow(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli)
+
+ described_class.initialize_slis!
+
+ if api
+ expect(Gitlab::Metrics::Sli::Apdex).to(
+ have_received(:initialize_sli).with(:global_search, array_including(*api_endpoint_labels))
+ )
+ expect(Gitlab::Metrics::Sli::ErrorRate).to(
+ have_received(:initialize_sli).with(:global_search, array_including(*api_endpoint_labels))
+ )
+ else
+ expect(Gitlab::Metrics::Sli::Apdex).not_to(
+ have_received(:initialize_sli).with(:global_search, array_including(*api_endpoint_labels))
+ )
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to(
+ have_received(:initialize_sli).with(:global_search, array_including(*api_endpoint_labels))
+ )
+ end
+
+ if web
+ expect(Gitlab::Metrics::Sli::Apdex).to(
+ have_received(:initialize_sli).with(:global_search, array_including(*web_endpoint_labels))
+ )
+ expect(Gitlab::Metrics::Sli::ErrorRate).to(
+ have_received(:initialize_sli).with(:global_search, array_including(*web_endpoint_labels))
+ )
+ else
+ expect(Gitlab::Metrics::Sli::Apdex).not_to(
+ have_received(:initialize_sli).with(:global_search, array_including(*web_endpoint_labels))
+ )
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to(
+ have_received(:initialize_sli).with(:global_search, array_including(*web_endpoint_labels))
+ )
+ end
+ end
+ end
+ end
end
describe '#record_apdex' do
diff --git a/spec/lib/gitlab/metrics/rails_slis_spec.rb b/spec/lib/gitlab/metrics/rails_slis_spec.rb
index 9da102fb8b8..32d3b7581f1 100644
--- a/spec/lib/gitlab/metrics/rails_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/rails_slis_spec.rb
@@ -1,27 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::RailsSlis do
- # Limit what routes we'll initialize so we don't have to load the entire thing
+RSpec.describe Gitlab::Metrics::RailsSlis, feature_category: :error_budgets do
before do
- api_route = API::API.routes.find do |route|
- API::Base.endpoint_id_for_route(route) == "GET /api/:version/version"
- end
-
- allow(Gitlab::RequestEndpoints).to receive(:all_api_endpoints).and_return([api_route])
- allow(Gitlab::RequestEndpoints).to receive(:all_controller_actions).and_return([[ProjectsController, 'index']])
allow(Gitlab::Graphql::KnownOperations).to receive(:default).and_return(Gitlab::Graphql::KnownOperations.new(%w(foo bar)))
end
describe '.initialize_request_slis!' do
- let(:possible_labels) do
+ let(:web_possible_labels) do
[
{
- endpoint_id: "GET /api/:version/version",
- feature_category: :not_owned,
- request_urgency: :default
- },
- {
endpoint_id: "ProjectsController#index",
feature_category: :projects,
request_urgency: :default
@@ -29,6 +17,28 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
]
end
+ # using the actual `#known_git_endpoints` here makes sure that we keep the
+ # list up to date as endpoints get removed
+ let(:git_possible_labels) do
+ described_class.__send__(:known_git_endpoints).map do |endpoint_id|
+ a_hash_including({
+ endpoint_id: endpoint_id
+ })
+ end
+ end
+
+ let(:api_possible_labels) do
+ [{
+ endpoint_id: "GET /api/:version/version",
+ feature_category: :not_owned,
+ request_urgency: :default
+ }]
+ end
+
+ let(:possible_request_labels) do
+ web_possible_labels + git_possible_labels + api_possible_labels
+ end
+
let(:possible_graphql_labels) do
['graphql:foo', 'graphql:bar', 'graphql:unknown'].map do |endpoint_id|
{
@@ -40,21 +50,57 @@ RSpec.describe Gitlab::Metrics::RailsSlis do
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(:rails_request, array_including(*possible_request_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
+ expect(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(:rails_request, array_including(*possible_request_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)
+ context "when initializeing for limited types" do
+ using RSpec::Parameterized::TableSyntax
- 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)
+ where(:git, :api, :web) do
+ [true, false].repeated_permutation(3).to_a
+ end
- described_class.initialize_request_slis!
+ with_them do
+ it 'initializes only with the expected labels', :aggregate_failures do
+ allow(Gitlab::Metrics::Environment).to receive(:git?).and_return(git)
+ allow(Gitlab::Metrics::Environment).to receive(:api?).and_return(api)
+ allow(Gitlab::Metrics::Environment).to receive(:web?).and_return(web)
+ allow(Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli)
+ allow(Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli)
+
+ described_class.initialize_request_slis!
+
+ if git
+ expect(Gitlab::Metrics::Sli::Apdex).to have_received(:initialize_sli).with(:rails_request, array_including(*git_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).to have_received(:initialize_sli).with(:rails_request, array_including(*git_possible_labels))
+ else
+ expect(Gitlab::Metrics::Sli::Apdex).not_to have_received(:initialize_sli).with(:rails_request, array_including(*git_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to have_received(:initialize_sli).with(:rails_request, array_including(*git_possible_labels))
+ end
+
+ if api
+ expect(Gitlab::Metrics::Sli::Apdex).to have_received(:initialize_sli).with(:rails_request, array_including(*api_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).to have_received(:initialize_sli).with(:rails_request, array_including(*api_possible_labels))
+ expect(Gitlab::Metrics::Sli::Apdex).to have_received(:initialize_sli).with(:graphql_query, array_including(*possible_graphql_labels))
+ else
+ expect(Gitlab::Metrics::Sli::Apdex).not_to have_received(:initialize_sli).with(:rails_request, array_including(*api_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to have_received(:initialize_sli).with(:rails_request, array_including(*api_possible_labels))
+ expect(Gitlab::Metrics::Sli::Apdex).to have_received(:initialize_sli).with(:graphql_query, [])
+ end
+
+ if web
+ expect(Gitlab::Metrics::Sli::Apdex).to have_received(:initialize_sli).with(:rails_request, array_including(*web_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).to have_received(:initialize_sli).with(:rails_request, array_including(*web_possible_labels))
+ else
+ expect(Gitlab::Metrics::Sli::Apdex).not_to have_received(:initialize_sli).with(:rails_request, array_including(*web_possible_labels))
+ expect(Gitlab::Metrics::Sli::ErrorRate).not_to have_received(:initialize_sli).with(:rails_request, array_including(*web_possible_labels))
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index 61c690b85e9..97b70b5a7fc 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -53,18 +53,6 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures, fea
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
-
context 'request is a health check endpoint' do
['/-/liveness', '/-/liveness/', '/-/%6D%65%74%72%69%63%73'].each do |path|
context "when path is #{path}" do
@@ -116,17 +104,6 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures, fea
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
end
context '@app.call throws exception' do
@@ -415,6 +392,35 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures, fea
end
end
+ context 'A request with urgency set on the env (from ETag-caching)' do
+ let(:env) do
+ { described_class::REQUEST_URGENCY_KEY => Gitlab::EndpointAttributes::Config::REQUEST_URGENCIES[:medium],
+ 'REQUEST_METHOD' => 'GET' }
+ end
+
+ it 'records the request with the correct urgency' do
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100, 100.1)
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :medium
+ },
+ success: true
+ )
+ expect(Gitlab::Metrics::RailsSlis.request_error_rate).to receive(:increment).with(
+ labels: {
+ feature_category: 'unknown',
+ endpoint_id: 'unknown',
+ request_urgency: :medium
+ },
+ error: false
+ )
+
+ subject.call(env)
+ end
+ end
+
context 'An unknown request' do
let(:env) do
{ 'REQUEST_METHOD' => 'GET' }
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 4569f3134ae..7ce5cbec18d 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
describe '.load_balancing_metric_counter_keys' do
context 'multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'has expected keys' do
@@ -91,7 +91,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
describe '.load_balancing_metric_duration_keys' do
context 'multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'has expected keys' do
diff --git a/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb b/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb
index b81000be62a..fb822c8d779 100644
--- a/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/ldap_spec.rb
@@ -101,11 +101,11 @@ RSpec.describe Gitlab::Metrics::Subscribers::Ldap, :request_store, feature_categ
it "tracks LDAP request duration" do
expect(transaction).to receive(:observe)
- .with(:gitlab_net_ldap_duration_seconds, 0.321, { name: "open" })
+ .with(:gitlab_net_ldap_duration_seconds, 0.000321, { name: "open" })
expect(transaction).to receive(:observe)
- .with(:gitlab_net_ldap_duration_seconds, 0.12, { name: "search" })
+ .with(:gitlab_net_ldap_duration_seconds, 0.00012, { name: "search" })
expect(transaction).to receive(:observe)
- .with(:gitlab_net_ldap_duration_seconds, 5.3, { name: "search" })
+ .with(:gitlab_net_ldap_duration_seconds, 0.0053, { name: "search" })
subscriber.observe_event(event_1)
subscriber.observe_event(event_2)
@@ -118,7 +118,7 @@ RSpec.describe Gitlab::Metrics::Subscribers::Ldap, :request_store, feature_categ
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
+ expect(Gitlab::SafeRequestStore[:net_ldap_duration_s]).to eq(0.005741) # (0.321 + 0.12 + 5.3) / 1000
end
end
end
diff --git a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
index 7f7efaffd9e..b401b7cc996 100644
--- a/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/load_balancing_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store do
+RSpec.describe Gitlab::Metrics::Subscribers::LoadBalancing, :request_store, feature_category: :pods do
let(:subscriber) { described_class.new }
describe '#caught_up_replica_pick' do
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index bed43c04460..aaa274e252d 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Middleware::Go do
+RSpec.describe Gitlab::Middleware::Go, feature_category: :source_code_management do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:env) do
diff --git a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
index d1d6ac80c40..6632a8106ca 100644
--- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
+++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe ::Gitlab::Nav::TopNavMenuItem do
+RSpec.describe ::Gitlab::Nav::TopNavMenuItem, feature_category: :navigation do
describe '.build' do
it 'builds a hash from the given args' do
item = {
diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb
index 7bce0788327..f7063f2c4f2 100644
--- a/spec/lib/gitlab/octokit/middleware_spec.rb
+++ b/spec/lib/gitlab/octokit/middleware_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe Gitlab::Octokit::Middleware do
+RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
- shared_examples 'Public URL' do
+ shared_examples 'Allowed URL' do
it 'does not raise an error' do
expect(app).to receive(:call).with(env)
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Octokit::Middleware do
end
end
- shared_examples 'Local URL' do
+ shared_examples 'Blocked URL' do
it 'raises an error' do
expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
end
@@ -24,7 +24,24 @@ RSpec.describe Gitlab::Octokit::Middleware do
context 'when the URL is a public URL' do
let(:env) { { url: 'https://public-url.com' } }
- it_behaves_like 'Public URL'
+ it_behaves_like 'Allowed URL'
+
+ context 'with failed address check' do
+ before do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+ allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError)
+ end
+
+ it_behaves_like 'Blocked URL'
+
+ context 'with disabled dns rebinding check' do
+ before do
+ stub_application_setting(dns_rebinding_protection_enabled: false)
+ end
+
+ it_behaves_like 'Allowed URL'
+ end
+ end
end
context 'when the URL is a localhost address' do
@@ -35,7 +52,7 @@ RSpec.describe Gitlab::Octokit::Middleware do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
end
- it_behaves_like 'Local URL'
+ it_behaves_like 'Blocked URL'
end
context 'when localhost requests are allowed' do
@@ -43,7 +60,7 @@ RSpec.describe Gitlab::Octokit::Middleware do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
end
- it_behaves_like 'Public URL'
+ it_behaves_like 'Allowed URL'
end
end
@@ -55,7 +72,7 @@ RSpec.describe Gitlab::Octokit::Middleware do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
end
- it_behaves_like 'Local URL'
+ it_behaves_like 'Blocked URL'
end
context 'when local network requests are allowed' do
@@ -63,7 +80,7 @@ RSpec.describe Gitlab::Octokit::Middleware do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
end
- it_behaves_like 'Public URL'
+ it_behaves_like 'Allowed URL'
end
end
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 563c97fa2cb..daef280dbaa 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -189,7 +189,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
it 'passes "args" hash as symbolized hash argument' do
hash_config = { 'name' => 'hash', 'args' => { 'custom' => 'format' } }
- expect(devise_config).to receive(:omniauth).with(:hash, custom: 'format')
+ expect(devise_config).to receive(:omniauth).with(:hash, { custom: 'format' })
subject.execute([hash_config])
end
@@ -197,7 +197,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
it 'normalizes a String strategy_class' do
hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::OAuth2Generic' } }
- expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)
+ expect(devise_config).to receive(:omniauth).with(:hash, { strategy_class: OmniAuth::Strategies::OAuth2Generic })
subject.execute([hash_config])
end
@@ -205,7 +205,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
it 'allows a class to be specified in strategy_class' do
hash_config = { 'name' => 'hash', 'args' => { strategy_class: OmniAuth::Strategies::OAuth2Generic } }
- expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)
+ expect(devise_config).to receive(:omniauth).with(:hash, { strategy_class: OmniAuth::Strategies::OAuth2Generic })
subject.execute([hash_config])
end
@@ -216,26 +216,10 @@ RSpec.describe Gitlab::OmniauthInitializer do
expect { subject.execute([hash_config]) }.to raise_error(NameError)
end
- it 'configures fail_with_empty_uid for shibboleth' do
- shibboleth_config = { 'name' => 'shibboleth', 'args' => {} }
-
- expect(devise_config).to receive(:omniauth).with(:shibboleth, fail_with_empty_uid: true)
-
- subject.execute([shibboleth_config])
- end
-
- it 'configures remote_sign_out_handler proc for authentiq' do
- authentiq_config = { 'name' => 'authentiq', 'args' => {} }
-
- expect(devise_config).to receive(:omniauth).with(:authentiq, remote_sign_out_handler: an_instance_of(Proc))
-
- subject.execute([authentiq_config])
- end
-
it 'configures on_single_sign_out proc for cas3' do
cas3_config = { 'name' => 'cas3', 'args' => {} }
- expect(devise_config).to receive(:omniauth).with(:cas3, on_single_sign_out: an_instance_of(Proc))
+ expect(devise_config).to receive(:omniauth).with(:cas3, { on_single_sign_out: an_instance_of(Proc) })
subject.execute([cas3_config])
end
@@ -247,11 +231,11 @@ RSpec.describe Gitlab::OmniauthInitializer do
}
expect(devise_config).to receive(:omniauth).with(
- :google_oauth2,
- access_type: "offline",
- approval_prompt: "",
- client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } }
- )
+ :google_oauth2, {
+ access_type: "offline",
+ approval_prompt: "",
+ client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } }
+ })
subject.execute([google_config])
end
@@ -263,10 +247,10 @@ RSpec.describe Gitlab::OmniauthInitializer do
}
expect(devise_config).to receive(:omniauth).with(
- :gitlab,
- client_options: { site: conf.dig('args', 'client_options', 'site') },
- authorize_params: { gl_auth_type: 'login' }
- )
+ :gitlab, {
+ client_options: { site: conf.dig('args', 'client_options', 'site') },
+ authorize_params: { gl_auth_type: 'login' }
+ })
subject.execute([conf])
end
@@ -275,9 +259,9 @@ RSpec.describe Gitlab::OmniauthInitializer do
conf = { 'name' => 'gitlab' }
expect(devise_config).to receive(:omniauth).with(
- :gitlab,
- authorize_params: { gl_auth_type: 'login' }
- )
+ :gitlab, {
+ authorize_params: { gl_auth_type: 'login' }
+ })
subject.execute([conf])
end
@@ -288,7 +272,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
expect(devise_config).to receive(:omniauth).with(
:gitlab,
'a',
- authorize_params: { gl_auth_type: 'login' }
+ { authorize_params: { gl_auth_type: 'login' } }
)
subject.execute([conf])
@@ -303,7 +287,7 @@ RSpec.describe Gitlab::OmniauthInitializer do
expect(devise_config).to receive(:omniauth).with(
:gitlab,
- authorize_params: { gl_auth_type: 'login' }
+ { authorize_params: { gl_auth_type: 'login' } }
)
subject.execute([conf])
diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb
index 6b24c8a8710..6b4b0e8fda6 100644
--- a/spec/lib/gitlab/other_markup_spec.rb
+++ b/spec/lib/gitlab/other_markup_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::OtherMarkup do
end
end
- def render(*args)
- described_class.render(*args)
+ def render(...)
+ described_class.render(...)
end
end
diff --git a/spec/lib/gitlab/pages/cache_control_spec.rb b/spec/lib/gitlab/pages/cache_control_spec.rb
index dd15aa87441..72240f52580 100644
--- a/spec/lib/gitlab/pages/cache_control_spec.rb
+++ b/spec/lib/gitlab/pages/cache_control_spec.rb
@@ -13,15 +13,23 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
end
it 'clears the cache' do
+ cached_keys = [
+ "pages_domain_for_#{type}_1_settings-hash",
+ "pages_domain_for_#{type}_1"
+ ]
+
+ expect(::Gitlab::AppLogger)
+ .to receive(:info)
+ .with(
+ message: 'clear pages cache',
+ pages_keys: cached_keys,
+ pages_type: type,
+ pages_id: 1
+ )
+
expect(Rails.cache)
.to receive(:delete_multi)
- .with(
- array_including(
- [
- "pages_domain_for_#{type}_1",
- "pages_domain_for_#{type}_1_settings-hash"
- ]
- ))
+ .with(cached_keys)
subject.clear_cache
end
@@ -31,13 +39,13 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do
describe '.for_namespace' do
subject(:cache_control) { described_class.for_namespace(1) }
- it_behaves_like 'cache_control', 'namespace'
+ it_behaves_like 'cache_control', :namespace
end
describe '.for_domain' do
subject(:cache_control) { described_class.for_domain(1) }
- it_behaves_like 'cache_control', 'domain'
+ it_behaves_like 'cache_control', :domain
end
describe '#cache_key' do
diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
index b1c4ffd6c29..dc32f471756 100644
--- a/spec/lib/gitlab/pagination/offset_pagination_spec.rb
+++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
@@ -308,8 +308,8 @@ RSpec.describe Gitlab::Pagination::OffsetPagination do
expect(subject).to receive(:header).with(*args, &block)
end
- def expect_no_header(*args, &block)
- expect(subject).not_to receive(:header).with(*args)
+ def expect_no_header(...)
+ expect(subject).not_to receive(:header).with(...)
end
def expect_message(method)
diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
index 8362c07baca..53c2db8a826 100644
--- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb
+++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::QuickActions::CommandDefinition do
- subject { described_class.new(:command) }
+ let(:mock_command) { :command }
+
+ subject { described_class.new(mock_command) }
describe "#all_names" do
context "when the command has aliases" do
@@ -74,7 +76,7 @@ RSpec.describe Gitlab::QuickActions::CommandDefinition do
context "when the command has types" do
before do
- subject.types = [Issue, Commit]
+ subject.types = [Issue, Commit, WorkItem]
end
context "when the command target type is allowed" do
@@ -82,6 +84,26 @@ RSpec.describe Gitlab::QuickActions::CommandDefinition do
opts[:quick_action_target] = Issue.new
expect(subject.available?(opts)).to be true
end
+
+ context 'when the command target type is Work Item' do
+ context 'when the command is not allowed' do
+ it "returns false" do
+ opts[:quick_action_target] = build(:work_item)
+ expect(subject.available?(opts)).to be false
+ end
+ end
+
+ context 'when the command is allowed' do
+ it "returns true" do
+ allow_next_instance_of(WorkItem) do |work_item|
+ allow(work_item).to receive(:supported_quick_action_commands).and_return([mock_command])
+ end
+
+ opts[:quick_action_target] = build(:work_item)
+ expect(subject.available?(opts)).to be true
+ end
+ end
+ end
end
context "when the command target type is not allowed" do
@@ -99,6 +121,9 @@ RSpec.describe Gitlab::QuickActions::CommandDefinition do
opts[:quick_action_target] = MergeRequest.new
expect(subject.available?(opts)).to be true
+
+ opts[:quick_action_target] = build(:work_item)
+ expect(subject.available?(opts)).to be true
end
end
end
diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb
index 82ff8a26199..64615c4d9ad 100644
--- a/spec/lib/gitlab/redis/cache_spec.rb
+++ b/spec/lib/gitlab/redis/cache_spec.rb
@@ -26,5 +26,22 @@ RSpec.describe Gitlab::Redis::Cache do
expect(described_class.active_support_config[:expires_in]).to eq(1.day)
end
+
+ context 'when encountering an error' do
+ let(:cache) { ActiveSupport::Cache::RedisCacheStore.new(**described_class.active_support_config) }
+
+ subject { cache.read('x') }
+
+ before do
+ described_class.with do |redis|
+ allow(redis).to receive(:get).and_raise(::Redis::CommandError)
+ end
+ end
+
+ it 'logs error' do
+ expect(::Gitlab::ErrorTracking).to receive(:log_exception)
+ subject
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/redis/cluster_rate_limiting_spec.rb b/spec/lib/gitlab/redis/cluster_rate_limiting_spec.rb
new file mode 100644
index 00000000000..3eba3233f08
--- /dev/null
+++ b/spec/lib/gitlab/redis/cluster_rate_limiting_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Redis::ClusterRateLimiting, feature_category: :redis do
+ include_examples "redis_new_instance_shared_examples", 'cluster_rate_limiting', Gitlab::Redis::Cache
+end
diff --git a/spec/lib/gitlab/redis/db_load_balancing_spec.rb b/spec/lib/gitlab/redis/db_load_balancing_spec.rb
new file mode 100644
index 00000000000..d633413ddec
--- /dev/null
+++ b/spec/lib/gitlab/redis/db_load_balancing_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Redis::DbLoadBalancing, feature_category: :scalability do
+ include_examples "redis_new_instance_shared_examples", 'db_load_balancing', Gitlab::Redis::SharedState
+ include_examples "redis_shared_examples"
+
+ describe '#pool' do
+ let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
+ let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
+
+ subject { described_class.pool }
+
+ before do
+ allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
+
+ # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists
+ allow(Gitlab::Redis::SharedState).to receive(:rails_root).and_return(mktmpdir)
+ allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_socket)
+ end
+
+ around do |example|
+ clear_pool
+ example.run
+ ensure
+ clear_pool
+ end
+
+ it 'instantiates an instance of MultiStore' do
+ subject.with do |redis_instance|
+ expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
+
+ expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
+ expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
+
+ expect(redis_instance.instance_name).to eq('DbLoadBalancing')
+ end
+ end
+
+ it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_db_load_balancing,
+ :use_primary_store_as_default_for_db_load_balancing
+ end
+
+ describe '#raw_config_hash' do
+ it 'has a legacy default URL' do
+ expect(subject).to receive(:fetch_config).and_return(false)
+
+ expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb b/spec/lib/gitlab/redis/duplicate_jobs_spec.rb
deleted file mode 100644
index 4d46a567032..00000000000
--- a/spec/lib/gitlab/redis/duplicate_jobs_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Redis::DuplicateJobs do
- # Note: this is a pseudo-store in front of `SharedState`, meant only as a tool
- # to move away from `Sidekiq.redis` for duplicate job data. Thus, we use the
- # same store configuration as the former.
- let(:instance_specific_config_file) { "config/redis.shared_state.yml" }
- let(:environment_config_file_name) { "GITLAB_REDIS_SHARED_STATE_CONFIG_FILE" }
-
- include_examples "redis_shared_examples"
-
- describe '#pool' do
- subject { described_class.pool }
-
- around do |example|
- clear_pool
- example.run
- ensure
- clear_pool
- end
-
- context 'store connection settings' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- before do
- allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host)
- allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket)
- end
-
- it 'instantiates an instance of MultiStore' do
- subject.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
-
- expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
- expect(redis_instance.primary_store.connection[:namespace]).to be_nil
- expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
- expect(redis_instance.secondary_store.connection[:namespace]).to eq("resque:gitlab")
-
- expect(redis_instance.instance_name).to eq('DuplicateJobs')
- end
- end
- end
-
- # Make sure they current namespace is respected for the secondary store but omitted from the primary
- context 'key namespaces' do
- let(:key) { 'key' }
- let(:value) { '123' }
-
- it 'writes keys to SharedState with no prefix, and to Queues with the "resque:gitlab:" prefix' do
- subject.with do |redis_instance|
- redis_instance.set(key, value)
- end
-
- Gitlab::Redis::SharedState.with do |redis_instance|
- expect(redis_instance.get(key)).to eq(value)
- end
-
- Gitlab::Redis::Queues.with do |redis_instance|
- expect(redis_instance.get("resque:gitlab:#{key}")).to eq(value)
- end
- end
- end
-
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_duplicate_jobs,
- :use_primary_store_as_default_for_duplicate_jobs
- end
-
- describe '#raw_config_hash' do
- it 'has a legacy default URL' do
- expect(subject).to receive(:fetch_config) { false }
-
- expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6382')
- end
- end
-
- describe '#store_name' do
- it 'returns the name of the SharedState store' do
- expect(described_class.store_name).to eq('SharedState')
- end
- end
-end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index f198ba90d0a..423a7e80ead 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -210,7 +210,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
- RSpec.shared_examples_for 'fallback read from the secondary store' do
+ RSpec.shared_examples_for 'fallback read from the non-default store' do
let(:counter) { Gitlab::Metrics::NullMetric.instance }
before do
@@ -218,7 +218,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
it 'fallback and execute on secondary instance' do
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+ expect(multi_store.fallback_store).to receive(name).with(*expected_args).and_call_original
subject
end
@@ -242,7 +242,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when fallback read from the secondary instance raises an exception' do
before do
- allow(secondary_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.fallback_store).to receive(name).with(*expected_args).and_raise(StandardError)
end
it 'fails with exception' do
@@ -283,10 +283,12 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject
end
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
+ unless params[:block]
+ it 'does not execute on the secondary store' do
+ expect(secondary_store).not_to receive(name)
- subject
+ subject
+ end
end
include_examples 'reads correct value'
@@ -294,7 +296,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
context 'when reading from primary instance is raising an exception' do
before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(multi_store.default_store).to receive(name).with(*expected_args).and_raise(StandardError)
allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
@@ -305,16 +307,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
subject
end
- include_examples 'fallback read from the secondary store'
+ include_examples 'fallback read from the non-default store'
end
- context 'when reading from empty primary instance' do
+ context 'when reading from empty default instance' do
before do
- # this ensures a cache miss without having to stub primary store
- primary_store.flushdb
+ # this ensures a cache miss without having to stub the default store
+ multi_store.default_store.flushdb
end
- include_examples 'fallback read from the secondary store'
+ include_examples 'fallback read from the non-default store'
end
context 'when the command is executed within pipelined block' do
@@ -344,8 +346,16 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
context 'when block is provided' do
- it 'yields to the block' do
+ it 'both stores yields to the block' do
expect(primary_store).to receive(name).and_yield(value)
+ expect(secondary_store).to receive(name).and_yield(value)
+
+ subject
+ end
+
+ it 'both stores to execute' do
+ expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
subject
end
@@ -412,7 +422,6 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
multi_store.mget(values) do |v|
multi_store.sadd(skey, v)
multi_store.scard(skey)
- v # mget receiving block returns the last line of the block for cache-hit check
end
end
@@ -422,9 +431,9 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
expect(primary_store).to receive(:send).with(:scard, skey).and_call_original
- expect(secondary_store).not_to receive(:send).with(:mget, values).and_call_original
- expect(secondary_store).not_to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
- expect(secondary_store).not_to receive(:send).with(:scard, skey).and_call_original
+ expect(secondary_store).to receive(:send).with(:mget, values).and_call_original
+ expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
+ expect(secondary_store).to receive(:send).with(:scard, skey).and_call_original
subject
end
@@ -451,7 +460,15 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
context 'when primary instance is default store' do
- it_behaves_like 'primary instance executes block'
+ it 'ensures only primary instance is executing the block' do
+ expect(secondary_store).not_to receive(:send)
+
+ expect(primary_store).to receive(:send).with(:mget, values).and_call_original
+ expect(primary_store).to receive(:send).with(:sadd, skey, %w[1 2 3]).and_call_original
+ expect(primary_store).to receive(:send).with(:scard, skey).and_call_original
+
+ subject
+ end
end
context 'when secondary instance is default store' do
@@ -464,9 +481,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
expect(secondary_store).to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
expect(secondary_store).to receive(:send).with(:scard, skey).and_call_original
- expect(primary_store).not_to receive(:send).with(:mget, values).and_call_original
- expect(primary_store).not_to receive(:send).with(:sadd, skey, %w[10 20 30]).and_call_original
- expect(primary_store).not_to receive(:send).with(:scard, skey).and_call_original
+ expect(primary_store).not_to receive(:send)
subject
end
@@ -700,7 +715,7 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
it 'runs block on correct Redis instance' do
if both_stores
expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).not_to receive(name)
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
expect(primary_store).to receive(:incr).with(rvalue)
expect(secondary_store).to receive(:incr).with(rvalue)
diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb
index e79c070df93..d82228426f0 100644
--- a/spec/lib/gitlab/redis/rate_limiting_spec.rb
+++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb
@@ -4,4 +4,21 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RateLimiting do
include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache
+
+ describe '.cache_store' do
+ context 'when encountering an error' do
+ subject { described_class.cache_store.read('x') }
+
+ before do
+ described_class.with do |redis|
+ allow(redis).to receive(:get).and_raise(::Redis::CommandError)
+ end
+ end
+
+ it 'logs error' do
+ expect(::Gitlab::ErrorTracking).to receive(:log_exception)
+ subject
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb
index b11e9ebf1f3..2c167a6eb62 100644
--- a/spec/lib/gitlab/redis/repository_cache_spec.rb
+++ b/spec/lib/gitlab/redis/repository_cache_spec.rb
@@ -4,46 +4,33 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do
include_examples "redis_new_instance_shared_examples", 'repository_cache', Gitlab::Redis::Cache
- include_examples "redis_shared_examples"
- describe '#pool' do
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
-
- subject { described_class.pool }
+ describe '#raw_config_hash' do
+ it 'has a legacy default URL' do
+ expect(subject).to receive(:fetch_config).and_return(false)
- before do
- allow(described_class).to receive(:config_file_name).and_return(config_new_format_host)
- allow(Gitlab::Redis::Cache).to receive(:config_file_name).and_return(config_new_format_socket)
+ expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380')
end
+ end
- around do |example|
- clear_pool
- example.run
- ensure
- clear_pool
+ describe '.cache_store' do
+ it 'has a default ttl of 8 hours' do
+ expect(described_class.cache_store.options[:expires_in]).to eq(8.hours)
end
- it 'instantiates an instance of MultiStore' do
- subject.with do |redis_instance|
- expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
+ context 'when encountering an error' do
+ subject { described_class.cache_store.read('x') }
- expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
- expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
-
- expect(redis_instance.instance_name).to eq('RepositoryCache')
+ before do
+ described_class.with do |redis|
+ allow(redis).to receive(:get).and_raise(::Redis::CommandError)
+ end
end
- end
- it_behaves_like 'multi store feature flags', :use_primary_and_secondary_stores_for_repository_cache,
- :use_primary_store_as_default_for_repository_cache
- end
-
- describe '#raw_config_hash' do
- it 'has a legacy default URL' do
- expect(subject).to receive(:fetch_config).and_return(false)
-
- expect(subject.send(:raw_config_hash)).to eq(url: 'redis://localhost:6380')
+ it 'logs error' do
+ expect(::Gitlab::ErrorTracking).to receive(:log_exception)
+ subject
+ end
end
end
end
diff --git a/spec/lib/gitlab/redis/sidekiq_status_spec.rb b/spec/lib/gitlab/redis/sidekiq_status_spec.rb
index e7cf229b494..bbfec13e6c8 100644
--- a/spec/lib/gitlab/redis/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/redis/sidekiq_status_spec.rb
@@ -14,10 +14,15 @@ RSpec.describe Gitlab::Redis::SidekiqStatus do
describe '#pool' do
let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
+ let(:rails_root) { mktmpdir }
subject { described_class.pool }
before do
+ # Override rails root to avoid having our fixtures overwritten by `redis.yml` if it exists
+ allow(Gitlab::Redis::SharedState).to receive(:rails_root).and_return(rails_root)
+ allow(Gitlab::Redis::Queues).to receive(:rails_root).and_return(rails_root)
+
allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(config_new_format_host)
allow(Gitlab::Redis::Queues).to receive(:config_file_name).and_return(config_new_format_socket)
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 9532a30144f..caca33704dd 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it_behaves_like 'project/group name chars regex'
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match("Users's something") }
+ it { is_expected.not_to match('users/something') }
end
shared_examples_for 'project name regex' do
@@ -28,6 +29,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match("Gitlab++") }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match("Users's something") }
+ it { is_expected.not_to match('users/something') }
end
describe '.project_name_regex' do
@@ -72,8 +74,47 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to eq("can contain only letters, digits, emojis, '_', '.', dash, space, parenthesis. It must start with letter, digit, emoji or '_'.") }
end
- describe '.bulk_import_namespace_path_regex' do
- subject { described_class.bulk_import_namespace_path_regex }
+ describe '.bulk_import_destination_namespace_path_regex_message' do
+ subject { described_class.bulk_import_destination_namespace_path_regex_message }
+
+ it {
+ is_expected
+ .to eq("cannot start with a non-alphanumeric character except for periods or underscores, " \
+ "can contain only alphanumeric characters, forward slashes, periods, and underscores, " \
+ "cannot end with a period or forward slash, and has a relative path structure " \
+ "with no http protocol chars or leading or trailing forward slashes")
+ }
+ end
+
+ describe '.bulk_import_destination_namespace_path_regex' do
+ subject { described_class.bulk_import_destination_namespace_path_regex }
+
+ it { is_expected.not_to match('?gitlab') }
+ it { is_expected.not_to match("Users's something") }
+ it { is_expected.not_to match('/source') }
+ it { is_expected.not_to match('http:') }
+ it { is_expected.not_to match('https:') }
+ it { is_expected.not_to match('example.com/?stuff=true') }
+ it { is_expected.not_to match('example.com:5000/?stuff=true') }
+ it { is_expected.not_to match('http://gitlab.example/gitlab-org/manage/import/gitlab-migration-test') }
+ it { is_expected.not_to match('_good_for_me!') }
+ it { is_expected.not_to match('good_for+you') }
+ it { is_expected.not_to match('source/') }
+ it { is_expected.not_to match('.source/full./path') }
+
+ it { is_expected.to match('source') }
+ it { is_expected.to match('.source') }
+ it { is_expected.to match('_source') }
+ it { is_expected.to match('source/full') }
+ it { is_expected.to match('source/full/path') }
+ it { is_expected.to match('.source/.full/.path') }
+ it { is_expected.to match('domain_namespace') }
+ it { is_expected.to match('gitlab-migration-test') }
+ it { is_expected.to match('') } # it is possible to pass an empty string for destination_namespace in bulk_import POST request
+ end
+
+ describe '.bulk_import_source_full_path_regex' do
+ subject { described_class.bulk_import_source_full_path_regex }
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match("Users's something") }
@@ -87,6 +128,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.not_to match('good_for+you') }
it { is_expected.not_to match('source/') }
it { is_expected.not_to match('.source/full./path') }
+ it { is_expected.not_to match('') }
it { is_expected.to match('source') }
it { is_expected.to match('.source') }
@@ -608,6 +650,7 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to match('1.0') }
it { is_expected.to match('1.0~alpha1') }
it { is_expected.to match('2:4.9.5+dfsg-5+deb10u1') }
+ it { is_expected.to match('0.0.0-806aa143-f0bf-4f27-be65-8e4fcb745f37') }
end
context 'dpkg errors' do
@@ -661,6 +704,22 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
end
end
+ describe '.debian_direct_upload_filename_regex' do
+ subject { described_class.debian_direct_upload_filename_regex }
+
+ it { is_expected.to match('libsample0_1.2.3~alpha2_amd64.deb') }
+ it { is_expected.to match('sample-dev_1.2.3~binary_amd64.deb') }
+ it { is_expected.to match('sample-udeb_1.2.3~alpha2_amd64.udeb') }
+
+ it { is_expected.not_to match('sample_1.2.3~alpha2_amd64.buildinfo') }
+ it { is_expected.not_to match('sample_1.2.3~alpha2_amd64.changes') }
+ it { is_expected.not_to match('sample_1.2.3~alpha2.dsc') }
+ it { is_expected.not_to match('sample_1.2.3~alpha2.tar.xz') }
+
+ # ensure right anchor
+ it { is_expected.not_to match('libsample0_1.2.3~alpha2_amd64.debu') }
+ end
+
describe '.helm_channel_regex' do
subject { described_class.helm_channel_regex }
@@ -977,21 +1036,6 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
- describe '.saved_reply_name_regex' do
- subject { described_class.saved_reply_name_regex }
-
- it { is_expected.to match('test') }
- it { is_expected.to match('test123') }
- it { is_expected.to match('test-test') }
- it { is_expected.to match('test-test_0123') }
- it { is_expected.not_to match('test test') }
- it { is_expected.not_to match('test-') }
- it { is_expected.not_to match('/z/test_') }
- it { is_expected.not_to match('.xtest_') }
- it { is_expected.not_to match('.xt.est_') }
- it { is_expected.not_to match('0test1') }
- end
-
describe '.sha256_regex' do
subject { described_class.sha256_regex }
@@ -1045,4 +1089,93 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.not_to match('random string') }
it { is_expected.not_to match('12321342545356434523412341245452345623453542345234523453245') }
end
+
+ describe 'code, html blocks, or html comment blocks regex' do
+ context 'code blocks' do
+ subject { described_class::MARKDOWN_CODE_BLOCK_REGEX }
+
+ let(:expected) { %(```code\nsome code\n\n>>>\nthat includes a multiline-blockquote\n>>>\n```) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ ```code
+ some code
+
+ >>>
+ that includes a multiline-blockquote
+ >>>
+ ```
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(```ruby\nsomething\n```)) }
+ it { is_expected.not_to match(%(must start in first column ```ruby\nsomething\n```)) }
+ it { is_expected.not_to match(%(```ruby must be multi-line ```)) }
+ it { expect(subject.match(markdown)[:code]).to eq expected }
+ end
+
+ context 'HTML blocks' do
+ subject { described_class::MARKDOWN_HTML_BLOCK_REGEX }
+
+ let(:expected) { %(<section>\n<p>paragraph</p>\n\n>>>\nthat includes a multiline-blockquote\n>>>\n</section>) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ <section>
+ <p>paragraph</p>
+
+ >>>
+ that includes a multiline-blockquote
+ >>>
+ </section>
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(<section>\nsomething\n</section>)) }
+ it { is_expected.not_to match(%(must start in first column <section>\nsomething\n</section>)) }
+ it { is_expected.not_to match(%(<section>must be multi-line</section>)) }
+ it { expect(subject.match(markdown)[:html]).to eq expected }
+ end
+
+ context 'HTML comment lines' do
+ subject { described_class::MARKDOWN_HTML_COMMENT_LINE_REGEX }
+
+ let(:expected) { %(<!-- an HTML comment -->) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ <!-- an HTML comment -->
+
+ more text
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(<!-- single line comment -->)) }
+ it { is_expected.not_to match(%(<!--\nblock comment\n-->)) }
+ it { is_expected.not_to match(%(must start in first column <!-- comment -->)) }
+ it { expect(subject.match(markdown)[:html_comment_line]).to eq expected }
+ end
+
+ context 'HTML comment blocks' do
+ subject { described_class::MARKDOWN_HTML_COMMENT_BLOCK_REGEX }
+
+ let(:expected) { %(<!-- the start of an HTML comment\n- [ ] list item commented out\n-->) }
+ let(:markdown) do
+ <<~MARKDOWN
+ Regular text
+
+ <!-- the start of an HTML comment
+ - [ ] list item commented out
+ -->
+ MARKDOWN
+ end
+
+ it { is_expected.to match(%(<!--\ncomment\n-->)) }
+ it { is_expected.not_to match(%(must start in first column <!--\ncomment\n-->)) }
+ it { expect(subject.match(markdown)[:html_comment_block]).to eq expected }
+ end
+ end
end
diff --git a/spec/lib/gitlab/repository_cache/preloader_spec.rb b/spec/lib/gitlab/repository_cache/preloader_spec.rb
index 71244dd41ed..e6fb0da6412 100644
--- a/spec/lib/gitlab/repository_cache/preloader_spec.rb
+++ b/spec/lib/gitlab/repository_cache/preloader_spec.rb
@@ -6,76 +6,51 @@ RSpec.describe Gitlab::RepositoryCache::Preloader, :use_clean_rails_redis_cachin
feature_category: :source_code_management do
let(:projects) { create_list(:project, 2, :repository) }
let(:repositories) { projects.map(&:repository) }
+ let(:cache) { Gitlab::RepositoryCache.store }
- before do
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- end
-
- shared_examples 'preload' do
- describe '#preload' do
- context 'when the values are already cached' do
- before do
- # Warm the cache but use a different model so they are not memoized
- repos = Project.id_in(projects).order(:id).map(&:repository)
-
- allow(repos[0]).to receive(:readme_path_gitaly).and_return('README.txt')
- allow(repos[1]).to receive(:readme_path_gitaly).and_return('README.md')
-
- repos.map(&:exists?)
- repos.map(&:readme_path)
- end
-
- it 'prevents individual cache reads for cached methods' do
- expect(cache).to receive(:read_multi).once.and_call_original
-
- described_class.new(repositories).preload(
- %i[exists? readme_path]
- )
-
- expect(cache).not_to receive(:read)
- expect(cache).not_to receive(:write)
+ describe '#preload' do
+ context 'when the values are already cached' do
+ before do
+ # Warm the cache but use a different model so they are not memoized
+ repos = Project.id_in(projects).order(:id).map(&:repository)
- expect(repositories[0].exists?).to eq(true)
- expect(repositories[0].readme_path).to eq('README.txt')
+ allow(repos[0].head_tree).to receive(:readme_path).and_return('README.txt')
+ allow(repos[1].head_tree).to receive(:readme_path).and_return('README.md')
- expect(repositories[1].exists?).to eq(true)
- expect(repositories[1].readme_path).to eq('README.md')
- end
+ repos.map(&:exists?)
+ repos.map(&:readme_path)
end
- context 'when values are not cached' do
- it 'reads and writes from cache individually' do
- described_class.new(repositories).preload(
- %i[exists? has_visible_content?]
- )
+ it 'prevents individual cache reads for cached methods' do
+ expect(cache).to receive(:read_multi).once.and_call_original
- expect(cache).to receive(:read).exactly(4).times
- expect(cache).to receive(:write).exactly(4).times
+ described_class.new(repositories).preload(
+ %i[exists? readme_path]
+ )
- repositories.each(&:exists?)
- repositories.each(&:has_visible_content?)
- end
- end
- end
- end
+ expect(cache).not_to receive(:read)
+ expect(cache).not_to receive(:write)
- context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is enabled' do
- let(:cache) { Gitlab::RepositoryCache.store }
+ expect(repositories[0].exists?).to eq(true)
+ expect(repositories[0].readme_path).to eq('README.txt')
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
+ expect(repositories[1].exists?).to eq(true)
+ expect(repositories[1].readme_path).to eq('README.md')
+ end
end
- it_behaves_like 'preload'
- end
+ context 'when values are not cached' do
+ it 'reads and writes from cache individually' do
+ described_class.new(repositories).preload(
+ %i[exists? has_visible_content?]
+ )
- context 'when use_primary_and_secondary_stores_for_repository_cache feature flag is disabled' do
- let(:cache) { Rails.cache }
+ expect(cache).to receive(:read).exactly(4).times
+ expect(cache).to receive(:write).exactly(4).times
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
+ repositories.each(&:exists?)
+ repositories.each(&:has_visible_content?)
+ end
end
-
- it_behaves_like 'preload'
end
end
diff --git a/spec/lib/gitlab/repository_hash_cache_spec.rb b/spec/lib/gitlab/repository_hash_cache_spec.rb
index d41bf45f72e..6b52c315a70 100644
--- a/spec/lib/gitlab/repository_hash_cache_spec.rb
+++ b/spec/lib/gitlab/repository_hash_cache_spec.rb
@@ -69,35 +69,20 @@ RSpec.describe Gitlab::RepositoryHashCache, :clean_gitlab_redis_cache do
end
end
- shared_examples "key?" do
- describe "#key?" do
- subject { cache.key?(:example, "test") }
+ describe "#key?" do
+ subject { cache.key?(:example, "test") }
- context "key exists" do
- before do
- cache.write(:example, test_hash)
- end
-
- it { is_expected.to be(true) }
+ context "key exists" do
+ before do
+ cache.write(:example, test_hash)
end
- context "key doesn't exist" do
- it { is_expected.to be(false) }
- end
+ it { is_expected.to be(true) }
end
- end
-
- context "when both multistore FF is enabled" do
- it_behaves_like "key?"
- end
- context "when both multistore FF is disabled" do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
+ context "key doesn't exist" do
+ it { is_expected.to be(false) }
end
-
- it_behaves_like "key?"
end
describe "#read_members" do
diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb
index c41a051bc42..8efbe053155 100644
--- a/spec/lib/gitlab/search/found_blob_spec.rb
+++ b/spec/lib/gitlab/search/found_blob_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Search::FoundBlob do
+RSpec.describe Gitlab::Search::FoundBlob, feature_category: :global_search do
let(:project) { create(:project, :public, :repository) }
describe 'parsing content results' do
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::Search::FoundBlob do
expect(subject.path).to eq('CHANGELOG')
expect(subject.basename).to eq('CHANGELOG')
expect(subject.ref).to eq('master')
+ expect(subject.matched_lines_count).to be_nil
expect(subject.startline).to eq(188)
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
end
diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
index 434642bf3ef..9f90e985207 100644
--- a/spec/lib/gitlab/sidekiq_death_handler_spec.rb
+++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
urgency :low
worker_has_external_dependencies!
worker_resource_boundary :cpu
- feature_category :users
+ feature_category :user_profile
end
end
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do
.to receive(:increment)
.with({ queue: 'test_queue', worker: 'TestWorker',
urgency: 'low', external_dependencies: 'yes',
- feature_category: 'users', boundary: 'cpu' })
+ feature_category: 'user_profile', boundary: 'cpu' })
described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index b6748d49739..6a515a2b8a5 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -77,7 +77,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
- shared_examples 'with Redis cookies' do
+ context 'with Redis cookies' do
+ def with_redis(&block)
+ Sidekiq.redis(&block)
+ end
+
let(:cookie_key) { "#{idempotency_key}:cookie:v2" }
let(:cookie) { get_redis_msgpack(cookie_key) }
@@ -385,41 +389,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
- context 'with multi-store feature flags turned on' do
- def with_redis(&block)
- Gitlab::Redis::DuplicateJobs.with(&block)
- end
-
- it 'use Gitlab::Redis::DuplicateJobs.with' do
- expect(Gitlab::Redis::DuplicateJobs).to receive(:with).and_call_original
- expect(Sidekiq).not_to receive(:redis)
-
- duplicate_job.check!
- end
-
- it_behaves_like 'with Redis cookies'
- end
-
- context 'when both multi-store feature flags are off' do
- def with_redis(&block)
- Sidekiq.redis(&block)
- end
-
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_duplicate_jobs: false)
- stub_feature_flags(use_primary_store_as_default_for_duplicate_jobs: false)
- end
-
- it 'use Sidekiq.redis' do
- expect(Sidekiq).to receive(:redis).and_call_original
- expect(Gitlab::Redis::DuplicateJobs).not_to receive(:with)
-
- duplicate_job.check!
- end
-
- it_behaves_like 'with Redis cookies'
- end
-
describe '#scheduled?' do
it 'returns false for non-scheduled jobs' do
expect(duplicate_job.scheduled?).to be(false)
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index f4664bcfef9..7e3ff46fae5 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::SlashCommands::Command do
+RSpec.describe Gitlab::SlashCommands::Command, feature_category: :integrations do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:chat_name) { double(:chat_name, user: user) }
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::SlashCommands::Command do
it 'displays the help message' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('The specified command is not valid')
- expect(subject[:text]).to match('/gitlab issue show')
+ expect(subject[:text]).to include('/gitlab [project name or alias] issue show <id>')
end
end
diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
index 7d36e67ddbf..15df2cea909 100644
--- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do
let_it_be(:other_project) { create(:project) }
let_it_be(:old_issue, reload: true) { create(:issue, project: project) }
- let(:new_issue) { Issues::MoveService.new(project: project, current_user: user).execute(old_issue, other_project) }
+ let(:new_issue) { Issues::MoveService.new(container: project, current_user: user).execute(old_issue, other_project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(new_issue).present(old_issue) }
diff --git a/spec/lib/gitlab/slug/path_spec.rb b/spec/lib/gitlab/slug/path_spec.rb
new file mode 100644
index 00000000000..9a7067e40a2
--- /dev/null
+++ b/spec/lib/gitlab/slug/path_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Slug::Path, feature_category: :not_owned do
+ describe '#generate' do
+ {
+ 'name': 'name',
+ 'james.atom@bond.com': 'james',
+ '--foobar--': 'foobar--',
+ '--foo_bar--': 'foo_bar--',
+ '--foo$^&_bar--': 'foo_bar--',
+ 'john@doe.com': 'john',
+ '-john+gitlab-ETC%.git@gmail.com': 'johngitlab-ETC',
+ 'this.is.git.atom.': 'this.is',
+ '#$%^.': 'blank',
+ '---.git#$.atom%@atom^.': 'blank', # use default when all characters are filtered out
+ '--gitlab--hey.git#$.atom%@atom^.': 'gitlab--hey'
+ }.each do |input, output|
+ it "yields a slug #{output} when given #{input}" do
+ slug = described_class.new(input).generate
+
+ expect(slug).to match(/\A#{output}\z/)
+ end
+ end
+ end
+
+ describe '#to_s' do
+ it 'presents with a cleaned slug' do
+ expect(described_class.new('---show-me-what-you.got.git').to_s).to match(/\Ashow-me-what-you.got\z/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb
index ab0611e6b6a..4203a76cbfb 100644
--- a/spec/lib/gitlab/time_tracking_formatter_spec.rb
+++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::TimeTrackingFormatter do
describe '#parse' do
- subject { described_class.parse(duration_string) }
+ let(:keep_zero) { false }
+
+ subject { described_class.parse(duration_string, keep_zero: keep_zero) }
context 'positive durations' do
let(:duration_string) { '3h 20m' }
@@ -25,6 +27,24 @@ RSpec.describe Gitlab::TimeTrackingFormatter do
expect(subject).to eq(576_000)
end
end
+
+ context 'when the duration is zero' do
+ let(:duration_string) { '0h' }
+
+ context 'when keep_zero is false' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when keep_zero is true' do
+ let(:keep_zero) { true }
+
+ it 'returns zero' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
end
describe '#output' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 05f7af7606d..0d037984799 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -174,6 +174,17 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
expect { subject }.to raise_error(described_class::BlockedUrlError)
end
+
+ context 'with HTTP_PROXY' do
+ before do
+ allow(Gitlab).to receive(:http_proxy_env?).and_return(true)
+ end
+
+ it_behaves_like 'validates URI and hostname' do
+ let(:expected_uri) { import_url }
+ let(:expected_hostname) { nil }
+ end
+ end
end
context 'when domain is too long' do
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
new file mode 100644
index 00000000000..afd8fccd56c
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ci_internal_pipelines_metric_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountCiInternalPipelinesMetric,
+feature_category: :service_ping do
+ let_it_be(:ci_pipeline_1) { create(:ci_pipeline, source: :external) }
+ let_it_be(:ci_pipeline_2) { create(:ci_pipeline, source: :push) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("ci_pipelines"."id") FROM "ci_pipelines" ' \
+ 'WHERE ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15) ' \
+ 'OR "ci_pipelines"."source" IS NULL)'
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+
+ context 'on Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ let(:expected_value) { -1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
new file mode 100644
index 00000000000..86f54c48666
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_created_manually_from_alerts_metric_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesCreatedManuallyFromAlertsMetric,
+feature_category: :service_ping do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:issue_with_alert) { create(:issue, :with_alert) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT("issues"."id") FROM "issues" ' \
+ 'INNER JOIN "alert_management_alerts" ON "alert_management_alerts"."issue_id" = "issues"."id" ' \
+ 'WHERE "issues"."author_id" != 99'
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+
+ context 'on Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ let(:expected_value) { -1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_candidates_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_candidates_metric_spec.rb
new file mode 100644
index 00000000000..7d7788737df
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_candidates_metric_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountMlCandidatesMetric, feature_category: :mlops do
+ let_it_be(:candidate) { create(:ml_candidates) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT("ml_candidates"."id") FROM "ml_candidates"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_experiments_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_experiments_metric_spec.rb
new file mode 100644
index 00000000000..887496ce39f
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_ml_experiments_metric_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountMlExperimentsMetric, feature_category: :mlops do
+ let_it_be(:candidate) { create(:ml_experiments) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT("ml_experiments"."id") FROM "ml_experiments"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_candidates_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_candidates_metric_spec.rb
new file mode 100644
index 00000000000..e5026ab6358
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_candidates_metric_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithMlCandidatesMetric,
+ feature_category: :mlops do
+ let_it_be(:project_without_candidates) { create(:project, :repository) }
+ let_it_be(:candidate) { create(:ml_candidates) }
+ let_it_be(:another_candidate) { create(:ml_candidates, experiment: candidate.experiment) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) do
+ 'SELECT COUNT(DISTINCT "ml_experiments"."ml_experiments.project_id") FROM "ml_experiments" WHERE ' \
+ '(EXISTS (SELECT 1 FROM "ml_candidates" WHERE ("ml_experiments"."id" = "ml_candidates"."experiment_id")))'
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_experiments_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_experiments_metric_spec.rb
new file mode 100644
index 00000000000..829245f785b
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_ml_experiments_metric_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithMlExperimentsMetric,
+ feature_category: :mlops do
+ let_it_be(:project_without_experiment) { create(:project, :repository) }
+ let_it_be(:experiment) { create(:ml_experiments) }
+ let_it_be(:another_experiment) { create(:ml_experiments, project: experiment.project) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT(DISTINCT "ml_experiments"."project_id") FROM "ml_experiments"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_monitor_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_monitor_enabled_metric_spec.rb
new file mode 100644
index 00000000000..d917dccd2b0
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_projects_with_monitor_enabled_metric_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountProjectsWithMonitorEnabledMetric,
+ feature_category: :metrics do
+ let_it_be(:projects) { create_list(:project, 3) }
+
+ let(:expected_value) { 2 }
+ let(:expected_query) do
+ 'SELECT COUNT("project_features"."id") FROM "project_features" WHERE "project_features"."monitor_access_level" != 0'
+ end
+
+ before_all do
+ # Monitor feature cannot have public visibility level. Therefore `ProjectFeature::PUBLIC` is missing here
+ projects[0].project_feature.update!(monitor_access_level: ProjectFeature::DISABLED)
+ projects[1].project_feature.update!(monitor_access_level: ProjectFeature::PRIVATE)
+ projects[2].project_feature.update!(monitor_access_level: ProjectFeature::ENABLED)
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_with_ml_candidates_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_with_ml_candidates_metric_spec.rb
new file mode 100644
index 00000000000..b25d61d0bd2
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_with_ml_candidates_metric_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersWithMlCandidatesMetric, feature_category: :mlops do
+ let_it_be(:user_without_candidates) { create(:user) }
+ let_it_be(:candidate) { create(:ml_candidates) }
+ let_it_be(:another_candidate) { create(:ml_candidates, user: candidate.user) }
+
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT(DISTINCT "ml_candidates"."user_id") FROM "ml_candidates"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb
new file mode 100644
index 00000000000..ed35b2c8cde
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/incoming_email_encrypted_secrets_enabled_metric_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::IncomingEmailEncryptedSecretsEnabledMetric,
+feature_category: :service_ping do
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do
+ let(:expected_value) { ::Gitlab::IncomingEmail.encrypted_secrets.active? }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/jira_active_integrations_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/jira_active_integrations_metric_spec.rb
new file mode 100644
index 00000000000..104fd18ba2d
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/jira_active_integrations_metric_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::JiraActiveIntegrationsMetric,
+ feature_category: :integrations do
+ let(:options) { { deployment_type: 'cloud', series: 0 } }
+ let(:integration_attributes) { { active: true, deployment_type: 'cloud' } }
+ let(:expected_value) { 3 }
+ let(:expected_query) do
+ 'SELECT COUNT("integrations"."id") FROM "integrations" ' \
+ 'INNER JOIN "jira_tracker_data" ON "jira_tracker_data"."integration_id" = "integrations"."id" ' \
+ 'WHERE "integrations"."type_new" = \'Integrations::Jira\' AND "integrations"."active" = TRUE ' \
+ 'AND "jira_tracker_data"."deployment_type" = 2'
+ end
+
+ before do
+ create_list :jira_integration, 3, integration_attributes
+
+ create :jira_integration, integration_attributes.merge(active: false)
+ create :jira_integration, integration_attributes.merge(deployment_type: 'server')
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query',
+ { options: { deployment_type: 'cloud' }, time_frame: 'all' }
+
+ it "raises an exception if option is not present" do
+ expect do
+ described_class.new(options: options.except(:deployment_type), time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{deployment_type .* must be one of})
+ end
+
+ it "raises an exception if option has invalid value" do
+ expect do
+ options[:deployment_type] = 'cloood'
+ described_class.new(options: options, time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{deployment_type .* must be one of})
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb
new file mode 100644
index 00000000000..d602eae3159
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/service_desk_email_encrypted_secrets_enabled_metric_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::ServiceDeskEmailEncryptedSecretsEnabledMetric,
+feature_category: :service_ping do
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' } do
+ let(:expected_value) { ::Gitlab::ServiceDeskEmail.encrypted_secrets.active? }
+ 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 83a4ea8e948..4f647c2700a 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
+RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator, feature_category: :service_ping do
include UsageDataHelpers
before do
@@ -43,9 +43,9 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
context 'joined relations' do
context 'counted attribute comes from source relation' 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/ }
+ # corresponding metric is collected with distinct_count(Release.with_milestones, :author_id)
+ let(:key_path) { 'usage_activity_by_stage.release.releases_with_milestones' }
+ let(:name_suggestion) { /count_distinct_author_id_from_releases_<with>_milestone_releases/ }
end
end
end
diff --git a/spec/lib/gitlab/usage/service_ping_report_spec.rb b/spec/lib/gitlab/usage/service_ping_report_spec.rb
index 7a37a31b195..730c05b7dcb 100644
--- a/spec/lib/gitlab/usage/service_ping_report_spec.rb
+++ b/spec/lib/gitlab/usage/service_ping_report_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching do
+RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching, feature_category: :service_ping do
include UsageDataHelpers
let(:usage_data) { { uuid: "1111", counts: { issue: 0 } }.deep_stringify_keys }
@@ -171,14 +171,25 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
let(:metric_definitions) { ::Gitlab::Usage::MetricDefinition.definitions }
it 'generates queries that match collected data', :aggregate_failures do
- message = "Expected %{query} result to match %{value} for %{key_path} metric"
+ message = "Expected %{query} result to match %{value} for %{key_path} metric (got %{payload_value} instead)"
metrics_queries_with_values.each do |key_path, query, value|
- value = type_cast_to_defined_type(value, metric_definitions[key_path.join('.')])
+ metric_definition = metric_definitions[key_path.join('.')]
+
+ # Skip broken metrics since they are usually overriden to return -1
+ next if metric_definition&.attributes&.fetch(:status) == 'broken'
+
+ value = type_cast_to_defined_type(value, metric_definition)
+ payload_value = service_ping_payload.dig(*key_path)
expect(value).to(
- eq(service_ping_payload.dig(*key_path)),
- message % { query: query, value: (value || 'NULL'), key_path: key_path.join('.') }
+ eq(payload_value),
+ message % {
+ query: query,
+ value: (value || 'NULL'),
+ payload_value: payload_value,
+ key_path: key_path.join('.')
+ }
)
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index f1115a8813d..11c6ea2fc9d 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -30,7 +30,6 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow }
let(:category) { described_class.to_s }
let(:action) { 'ci_templates_unique' }
let(:namespace) { project.namespace }
diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
index bfbabd858f0..f8a4603c1f8 100644
--- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
@@ -65,20 +65,4 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
end
end
end
-
- it 'can return the count of actions per user deduplicated' do
- described_class.track_web_ide_edit_action(author: user1, project: project)
- described_class.track_live_preview_edit_action(author: user1, project: project)
- described_class.track_snippet_editor_edit_action(author: user1, project: project)
- described_class.track_sfe_edit_action(author: user1, project: project)
- described_class.track_web_ide_edit_action(author: user2, time: time - 2.days, project: project)
- described_class.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project)
- described_class.track_live_preview_edit_action(author: user2, time: time - 2.days, project: project)
- described_class.track_live_preview_edit_action(author: user3, time: time - 3.days, project: project)
- described_class.track_snippet_editor_edit_action(author: user3, time: time - 3.days, project: project)
- described_class.track_sfe_edit_action(author: user3, time: time - 3.days, project: project)
-
- expect(described_class.count_edit_using_editor(date_from: time, date_to: Date.today)).to eq(1)
- expect(described_class.count_edit_using_editor(date_from: time - 5.days, date_to: Date.tomorrow)).to eq(3)
- end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 1d980c48c72..f955fd265e5 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
@@ -64,7 +64,6 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
"name" => "ce_event",
"redis_slot" => "analytics",
"category" => "analytics",
- "expiry" => 84,
"aggregation" => "weekly"
}
end
@@ -106,13 +105,13 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly", feature_flag: feature },
- { name: daily_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "daily" },
+ { name: weekly_event, redis_slot: "analytics", category: analytics_category, aggregation: "weekly", feature_flag: feature },
+ { name: daily_event, redis_slot: "analytics", category: analytics_category, aggregation: "daily" },
{ name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" },
{ name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" },
{ name: no_slot, category: global_category, aggregation: "daily" },
{ name: different_aggregation, category: global_category, aggregation: "monthly" },
- { name: context_event, category: other_category, expiry: 6, aggregation: 'weekly' }
+ { name: context_event, category: other_category, aggregation: 'weekly' }
].map(&:with_indifferent_access)
end
@@ -196,7 +195,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'tracks events with multiple values' do
values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values, expiry: 84.days)
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values,
+ expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
described_class.track_event(:g_analytics_contribution, values: values)
end
@@ -233,20 +233,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
context 'for weekly events' do
- it 'sets the keys in Redis to expire automatically after the given expiry time' do
- described_class.track_event("g_analytics_contribution", values: entity1)
-
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a
- expect(keys).not_to be_empty
-
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
- end
- end
- end
-
- it 'sets the keys in Redis to expire automatically after 6 weeks by default' do
+ it 'sets the keys in Redis to expire' do
described_class.track_event("g_compliance_dashboard", values: entity1)
Gitlab::Redis::SharedState.with do |redis|
@@ -254,27 +241,14 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(keys).not_to be_empty
keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(6.weeks)
+ expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
end
end
end
end
context 'for daily events' do
- it 'sets the keys in Redis to expire after the given expiry time' do
- described_class.track_event("g_analytics_search", values: entity1)
-
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "*-g_{analytics}_search").to_a
- expect(keys).not_to be_empty
-
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(84.days)
- end
- end
- end
-
- it 'sets the keys in Redis to expire after 29 days by default' do
+ it 'sets the keys in Redis to expire' do
described_class.track_event("no_slot", values: entity1)
Gitlab::Redis::SharedState.with do |redis|
@@ -282,7 +256,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect(keys).not_to be_empty
keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(29.days)
+ expect(redis.ttl(key)).to be_within(5.seconds).of(described_class::DEFAULT_DAILY_KEY_EXPIRY_LENGTH)
end
end
end
@@ -302,7 +276,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it 'tracks events with multiple values' do
values = [entity1, entity2]
- expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/, value: values, expiry: 84.days)
+ expect(Gitlab::Redis::HLL).to receive(:add).with(key: /g_{analytics}_contribution/,
+ value: values,
+ expiry: described_class::DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH)
described_class.track_event_in_context(:g_analytics_contribution, values: values, context: default_context)
end
diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
index 032a5e78385..33e0d446fca 100644
--- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
@@ -284,6 +284,16 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
end
+ context 'for Issue design comment removed actions' do
+ it_behaves_like 'daily tracked issuable snowplow and service ping events with project' do
+ let(:action) { described_class::ISSUE_DESIGN_COMMENT_REMOVED }
+
+ def track_action(params)
+ described_class.track_issue_design_comment_removed_action(**params)
+ end
+ end
+ end
+
it 'can return the count of actions per user deduplicated', :aggregate_failures do
described_class.track_issue_title_changed_action(author: user1, project: project)
described_class.track_issue_description_changed_action(author: user1, project: project)
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 9a1ffd8d01d..42aa84c2c3e 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
@@ -70,8 +70,8 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
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 }
+ let(:label) { 'redis_hll_counters.code_review.i_code_review_user_create_mr_monthly' }
+ let(:property) { described_class::MR_USER_CREATE_ACTION }
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
index b0e5bd18b66..d79fa66b983 100644
--- a/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
@@ -34,46 +34,17 @@ RSpec.describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_sha
it_behaves_like 'counter examples', 'pipelines'
end
- describe 'previews counter' do
- let(:setting_enabled) { true }
-
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: setting_enabled)
- end
-
- context 'when web ide clientside preview is enabled' do
- it_behaves_like 'counter examples', 'previews'
- end
-
- context 'when web ide clientside preview is not enabled' do
- let(:setting_enabled) { false }
-
- it 'does not increment the counter' do
- redis_key = 'WEB_IDE_PREVIEWS_COUNT'
- expect(described_class.total_count(redis_key)).to eq(0)
-
- 2.times { described_class.increment_previews_count }
-
- expect(described_class.total_count(redis_key)).to eq(0)
- end
- end
- end
-
describe '.totals' do
commits = 5
merge_requests = 3
views = 2
- previews = 4
terminals = 1
pipelines = 2
before do
- stub_application_setting(web_ide_clientside_preview_enabled: true)
-
commits.times { described_class.increment_commits_count }
merge_requests.times { described_class.increment_merge_requests_count }
views.times { described_class.increment_views_count }
- previews.times { described_class.increment_previews_count }
terminals.times { described_class.increment_terminals_count }
pipelines.times { described_class.increment_pipelines_count }
end
@@ -83,7 +54,6 @@ RSpec.describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_sha
web_ide_commits: commits,
web_ide_views: views,
web_ide_merge_requests: merge_requests,
- web_ide_previews: previews,
web_ide_terminals: terminals
)
end
diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb
index 34f8e5b2a2f..6391b003096 100644
--- a/spec/lib/gitlab/usage_data_metrics_spec.rb
+++ b/spec/lib/gitlab/usage_data_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::UsageDataMetrics, :with_license do
+RSpec.describe Gitlab::UsageDataMetrics, :with_license, feature_category: :service_ping do
describe '.uncached_data' do
subject { described_class.uncached_data }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 592ac280d32..5325ef5b5dd 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -557,9 +557,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2)
expect(count_data[:incident_issues]).to eq(4)
- expect(count_data[:issues_created_gitlab_alerts]).to eq(1)
expect(count_data[:issues_created_from_alerts]).to eq(3)
- expect(count_data[:issues_created_manually_from_alerts]).to eq(1)
expect(count_data[:alert_bot_incident_issues]).to eq(4)
expect(count_data[:clusters_enabled]).to eq(6)
expect(count_data[:project_clusters_enabled]).to eq(4)
@@ -739,7 +737,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled)
expect(subject[:dependency_proxy_enabled]).to eq(Gitlab.config.dependency_proxy.enabled)
expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
- expect(subject[:web_ide_clientside_preview_enabled]).to eq(Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?)
expect(subject[:grafana_link_enabled]).to eq(Gitlab::CurrentSettings.grafana_enabled?)
expect(subject[:gitpod_enabled]).to eq(Gitlab::CurrentSettings.gitpod_enabled?)
end
@@ -1102,8 +1099,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
{
action_monthly_active_users_web_ide_edit: 2,
action_monthly_active_users_sfe_edit: 2,
- action_monthly_active_users_snippet_editor_edit: 2,
- action_monthly_active_users_ide_edit: 3
+ action_monthly_active_users_snippet_editor_edit: 2
}
)
end
@@ -1138,20 +1134,4 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
expect(result).to be_nil
end
end
-
- context 'on Gitlab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- describe '.system_usage_data' do
- subject { described_class.system_usage_data }
-
- it 'returns fallback value for disabled metrics' do
- expect(subject[:counts][:ci_internal_pipelines]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- expect(subject[:counts][:issues_created_gitlab_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- expect(subject[:counts][:issues_created_manually_from_alerts]).to eq(Gitlab::Utils::UsageData::FALLBACK)
- end
- end
- end
end
diff --git a/spec/lib/gitlab/utils/email_spec.rb b/spec/lib/gitlab/utils/email_spec.rb
new file mode 100644
index 00000000000..d7a881d8655
--- /dev/null
+++ b/spec/lib/gitlab/utils/email_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.obfuscated_email' do
+ where(:input, :output) do
+ 'alex@gitlab.com' | 'al**@g*****.com'
+ 'alex@gl.co.uk' | 'al**@g****.uk'
+ 'a@b.c' | 'a@b.c'
+ 'q@example.com' | 'q@e******.com'
+ 'q@w.' | 'q@w.'
+ 'a@b' | 'a@b'
+ 'no mail' | 'no mail'
+ end
+
+ with_them do
+ it { expect(described_class.obfuscated_email(input)).to eq(output) }
+ end
+
+ context 'when deform is active' do
+ where(:input, :output) do
+ 'alex@gitlab.com' | 'al*****@g*****.c**'
+ 'alex@gl.co.uk' | 'al*****@g*****.u**'
+ 'a@b.c' | 'aa*****@b*****.c**'
+ 'qqwweerrttyy@example.com' | 'qq*****@e*****.c**'
+ 'getsuperfancysupport@paywhatyouwant.accounting' | 'ge*****@p*****.a**'
+ 'q@example.com' | 'qq*****@e*****.c**'
+ 'q@w.' | 'q@w.'
+ 'a@b' | 'a@b'
+ 'no mail' | 'no mail'
+ end
+
+ with_them do
+ it { expect(described_class.obfuscated_email(input, deform: true)).to eq(output) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 80b2ec63af9..102d608072b 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe Gitlab::Utils do
delegate :to_boolean, :boolean_to_yes_no, :slugify, :which,
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
- :append_path, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!, :decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class
+ :append_path, :remove_leading_slashes, :check_path_traversal!, :allowlisted?, :check_allowed_absolute_path!,
+ :decode_path, :ms_to_round_sec, :check_allowed_absolute_path_and_path_traversal!, to: :described_class
describe '.check_path_traversal!' do
it 'detects path traversal in string without any separators' do
@@ -378,6 +379,23 @@ RSpec.describe Gitlab::Utils do
end
end
+ describe '.remove_leading_slashes' do
+ where(:str, :result) do
+ '/foo/bar' | 'foo/bar'
+ '//foo/bar' | 'foo/bar'
+ '/foo/bar/' | 'foo/bar/'
+ 'foo/bar' | 'foo/bar'
+ '' | ''
+ nil | ''
+ end
+
+ with_them do
+ it 'removes leading slashes' do
+ expect(remove_leading_slashes(str)).to eq(result)
+ end
+ end
+ end
+
describe '.ensure_utf8_size' do
context 'string is has less bytes than expected' do
it 'backfills string with null characters' do
diff --git a/spec/lib/initializer_connections_spec.rb b/spec/lib/initializer_connections_spec.rb
index 4ca283c4f22..e69aa0aa821 100644
--- a/spec/lib/initializer_connections_spec.rb
+++ b/spec/lib/initializer_connections_spec.rb
@@ -3,15 +3,20 @@
require 'spec_helper'
RSpec.describe InitializerConnections do
- describe '.with_disabled_database_connections', :reestablished_active_record_base do
+ describe '.raise_if_new_database_connection', :reestablished_active_record_base do
+ before do
+ ActiveRecord::Base.connection_handler.clear_active_connections!
+ ActiveRecord::Base.connection_handler.flush_idle_connections!
+ end
+
def block_with_database_call
- described_class.with_disabled_database_connections do
+ described_class.raise_if_new_database_connection do
Project.first
end
end
def block_with_error
- described_class.with_disabled_database_connections do
+ described_class.raise_if_new_database_connection do
raise "oops, an error"
end
end
@@ -20,6 +25,12 @@ RSpec.describe InitializerConnections do
expect { block_with_database_call }.to raise_error(/Database connection should not be called during initializer/)
end
+ it 'prevents any database connection re-use within the block' do
+ Project.connection # establish a connection first, it will be used inside the block
+
+ expect { block_with_database_call }.to raise_error(/Database connection should not be called during initializer/)
+ end
+
it 'does not prevent database connection if SKIP_RAISE_ON_INITIALIZE_CONNECTIONS is set' do
stub_env('SKIP_RAISE_ON_INITIALIZE_CONNECTIONS', '1')
@@ -33,31 +44,34 @@ RSpec.describe InitializerConnections do
end
it 'restores original connection handler' do
- # rubocop:disable Database/MultipleDatabases
original_handler = ActiveRecord::Base.connection_handler
expect { block_with_database_call }.to raise_error(/Database connection should not be called during initializer/)
expect(ActiveRecord::Base.connection_handler).to eq(original_handler)
- # rubocop:enabled Database/MultipleDatabases
end
it 'restores original connection handler even there is an error' do
- # rubocop:disable Database/MultipleDatabases
original_handler = ActiveRecord::Base.connection_handler
expect { block_with_error }.to raise_error(/an error/)
expect(ActiveRecord::Base.connection_handler).to eq(original_handler)
- # rubocop:enabled Database/MultipleDatabases
end
- it 'raises if any new connection_pools are established in the block' do
+ it 'does not raise if connection_pool is retrieved in the block' do
+ # connection_pool, connection_db_config doesn't connect to database, so it's OK
+ expect do
+ described_class.raise_if_new_database_connection do
+ ApplicationRecord.connection_pool
+ end
+ end.not_to raise_error
+
expect do
- described_class.with_disabled_database_connections do
- ApplicationRecord.connects_to database: { writing: :main, reading: :main }
+ described_class.raise_if_new_database_connection do
+ Ci::ApplicationRecord.connection_pool
end
- end.to raise_error(/Unxpected connection_pools/)
+ end.not_to raise_error
end
end
end
diff --git a/spec/lib/marginalia_spec.rb b/spec/lib/marginalia_spec.rb
index 5f405e71d79..43c3e4d67e5 100644
--- a/spec/lib/marginalia_spec.rb
+++ b/spec/lib/marginalia_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe 'Marginalia spec' do
end
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'generates a query that includes the component and value' do
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 569e6a3a7c6..82eede96deb 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -272,7 +272,7 @@ RSpec.describe ObjectStorage::DirectUpload do
it 'uses only strings in query parameters' do
expect(direct_upload.send(:connection)).to receive(:signed_url).at_least(:once) do |params|
if params[:query]
- expect(params[:query].keys.all? { |key| key.is_a?(String) }).to be_truthy
+ expect(params[:query].keys.all?(String)).to be_truthy
end
end
diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb
index fc768bdcb82..aeaf28c7e7d 100644
--- a/spec/lib/peek/views/active_record_spec.rb
+++ b/spec/lib/peek/views/active_record_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Peek::Views::ActiveRecord, :request_store do
+RSpec.describe Peek::Views::ActiveRecord, :request_store, feature_category: :database do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
let(:connection_replica) { double(:connection_replica) }
diff --git a/spec/lib/release_highlights/validator/entry_spec.rb b/spec/lib/release_highlights/validator/entry_spec.rb
index b8b745ac8cd..63b753bd871 100644
--- a/spec/lib/release_highlights/validator/entry_spec.rb
+++ b/spec/lib/release_highlights/validator/entry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ReleaseHighlights::Validator::Entry do
+RSpec.describe ReleaseHighlights::Validator::Entry, type: :model, feature_category: :onboarding do
subject(:entry) { described_class.new(document.root.children.first) }
let(:document) { YAML.parse(File.read(yaml_path)) }
@@ -22,34 +22,26 @@ RSpec.describe ReleaseHighlights::Validator::Entry do
context 'with an invalid entry' do
let(:yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
- it 'returns line numbers in errors' do
- subject.valid?
-
- expect(entry.errors[:available_in].first).to match('(line 6)')
- end
+ it { is_expected.to be_invalid }
end
context 'with a blank entry' do
- it 'validate presence of name, description and stage' do
- subject.valid?
-
- expect(subject.errors[:name]).not_to be_empty
- expect(subject.errors[:description]).not_to be_empty
- expect(subject.errors[:stage]).not_to be_empty
- expect(subject.errors[:available_in]).not_to be_empty
- end
-
- it 'validates boolean value of "self-managed" and "gitlab-com"' do
- allow(entry).to receive(:value_for).with(:'self-managed').and_return('nope')
- allow(entry).to receive(:value_for).with(:'gitlab-com').and_return('yerp')
-
- subject.valid?
-
- expect(subject.errors[:'self-managed']).to include(/must be a boolean/)
- expect(subject.errors[:'gitlab-com']).to include(/must be a boolean/)
- end
-
- it 'validates URI of "url" and "image_url"' do
+ it { is_expected.to validate_presence_of(:name).with_message(/can't be blank \(line [0-9]+\)/) }
+ it { is_expected.to validate_presence_of(:description).with_message(/can't be blank/) }
+ it { is_expected.to validate_presence_of(:stage).with_message(/can't be blank/) }
+ it { is_expected.to validate_presence_of(:self_managed).with_message(/must be a boolean/) }
+ it { is_expected.to validate_presence_of(:gitlab_com).with_message(/must be a boolean/) }
+ it { is_expected.to allow_value(nil).for(:image_url) }
+
+ it {
+ is_expected.to validate_presence_of(:available_in)
+ .with_message(/must be one of \["Free", "Premium", "Ultimate"\]/)
+ }
+
+ it { is_expected.to validate_presence_of(:published_at).with_message(/must be valid Date/) }
+ it { is_expected.to validate_numericality_of(:release).with_message(/is not a number/) }
+
+ it 'validates URI of "documentation_link" and "image_url"' do
stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
allow(entry).to receive(:value_for).with(:image_url).and_return('https://foobar.x/images/ci/gitlab-ci-cd-logo_2x.png')
allow(entry).to receive(:value_for).with(:documentation_link).and_return('')
@@ -60,16 +52,8 @@ RSpec.describe ReleaseHighlights::Validator::Entry do
expect(subject.errors[:image_url]).to include(/is blocked: Host cannot be resolved or invalid/)
end
- it 'validates release is numerical' do
- allow(entry).to receive(:value_for).with(:release).and_return('one')
-
- subject.valid?
-
- expect(subject.errors[:release]).to include(/is not a number/)
- end
-
it 'validates published_at is a date' do
- allow(entry).to receive(:value_for).with(:published_at).and_return('christmas day')
+ allow(entry).to receive(:published_at).and_return('christmas day')
subject.valid?
@@ -77,7 +61,7 @@ RSpec.describe ReleaseHighlights::Validator::Entry do
end
it 'validates available_in are included in list' do
- allow(entry).to receive(:value_for).with(:available_in).and_return(['ALL'])
+ allow(entry).to receive(:available_in).and_return(['ALL'])
subject.valid?
diff --git a/spec/lib/release_highlights/validator_spec.rb b/spec/lib/release_highlights/validator_spec.rb
index dd1b3aa4803..7cfeffb095a 100644
--- a/spec/lib/release_highlights/validator_spec.rb
+++ b/spec/lib/release_highlights/validator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ReleaseHighlights::Validator do
+RSpec.describe ReleaseHighlights::Validator, feature_category: :experimentation_adoption do
let(:validator) { described_class.new(file: yaml_path) }
let(:yaml_path) { 'spec/fixtures/whats_new/valid.yml' }
let(:invalid_yaml_path) { 'spec/fixtures/whats_new/invalid.yml' }
diff --git a/spec/lib/service_ping/build_payload_spec.rb b/spec/lib/service_ping/build_payload_spec.rb
index b10c9fd5bc0..6c37168f5a0 100644
--- a/spec/lib/service_ping/build_payload_spec.rb
+++ b/spec/lib/service_ping/build_payload_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ServicePing::BuildPayload do
+RSpec.describe ServicePing::BuildPayload, feature_category: :service_ping do
describe '#execute', :without_license do
subject(:service_ping_payload) { described_class.new.execute }
diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
deleted file mode 100644
index 4ae29f28f3a..00000000000
--- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
- let_it_be(:project) { build(:project) }
- let_it_be(:learn_gitlab_enabled) { true }
-
- let(:context) do
- Sidebars::Projects::Context.new(
- current_user: nil,
- container: project,
- learn_gitlab_enabled: learn_gitlab_enabled
- )
- end
-
- subject { described_class.new(context) }
-
- it 'does not contain any sub menu' do
- expect(subject.has_items?).to be false
- end
-
- describe '#nav_link_html_options' do
- let_it_be(:data_tracking) do
- {
- class: 'home',
- data: {
- track_label: 'learn_gitlab'
- }
- }
- end
-
- specify do
- expect(subject.nav_link_html_options).to eq(data_tracking)
- end
- end
-
- describe '#render?' do
- context 'when learn gitlab experiment is enabled' do
- it 'returns true' do
- expect(subject.render?).to eq true
- end
- end
-
- context 'when learn gitlab experiment is disabled' do
- let(:learn_gitlab_enabled) { false }
-
- it 'returns false' do
- expect(subject.render?).to eq false
- end
- end
- end
-
- describe '#has_pill?' do
- context 'when learn gitlab experiment is enabled' do
- it 'returns true' do
- expect(subject.has_pill?).to eq true
- end
- end
-
- context 'when learn gitlab experiment is disabled' do
- let(:learn_gitlab_enabled) { false }
-
- it 'returns false' do
- expect(subject.has_pill?).to eq false
- end
- end
- end
-
- describe '#pill_count' do
- it 'returns pill count' do
- expect_next_instance_of(Onboarding::Completion) do |onboarding|
- expect(onboarding).to receive(:percentage).and_return(20)
- end
-
- expect(subject.pill_count).to eq '20%'
- 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 e7aa2b7edca..40ca2107698 100644
--- a/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb
@@ -48,20 +48,13 @@ RSpec.describe Sidebars::Projects::Menus::RepositoryMenu, feature_category: :sou
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 }
+ where(:ref_type, :link) do
+ nil | lazy { "#{route}?ref_type=heads" }
+ 'heads' | lazy { "#{route}?ref_type=heads" }
+ 'tags' | lazy { "#{route}?ref_type=tags" }
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
diff --git a/spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb
index b3251a54178..8941c11006e 100644
--- a/spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb
+++ b/spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb
@@ -3,10 +3,55 @@
require 'spec_helper'
RSpec.describe Sidebars::YourWork::Menus::MergeRequestsMenu, feature_category: :navigation do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
subject { described_class.new(context) }
include_examples 'menu item shows pill based on count', :assigned_open_merge_requests_count
+
+ describe 'submenu items' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:order, :title, :key) do
+ 0 | 'Assigned' | :assigned
+ 1 | 'Review requests' | :review_requested
+ end
+
+ with_them do
+ let(:item) { subject.renderable_items[order] }
+
+ it 'renders items in the right order' do
+ expect(item.title).to eq title
+ end
+
+ context 'when there are no MR counts' do
+ before do
+ allow(user).to receive(:assigned_open_merge_requests_count).and_return(0)
+ allow(user).to receive(:review_requested_open_merge_requests_count).and_return(0)
+ end
+
+ it 'shows a pill even though count is zero' do
+ expect(item.has_pill?).to eq true
+ expect(item.pill_count).to eq 0
+ end
+ end
+
+ context 'when there are MR counts' do
+ let(:non_zero_counts) { { assigned: 2, review_requested: 3 } }
+
+ before do
+ allow(user).to receive(:assigned_open_merge_requests_count).and_return(non_zero_counts[:assigned])
+ allow(user).to receive(:review_requested_open_merge_requests_count)
+ .and_return(non_zero_counts[:review_requested])
+ end
+
+ it 'shows a pill with the correct count' do
+ expect(item.has_pill?).to eq true
+ expect(item.pill_count).to eq non_zero_counts[key]
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/slack_markdown_sanitizer_spec.rb b/spec/lib/slack_markdown_sanitizer_spec.rb
new file mode 100644
index 00000000000..f4042439213
--- /dev/null
+++ b/spec/lib/slack_markdown_sanitizer_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SlackMarkdownSanitizer, feature_category: :integrations do
+ describe '.sanitize' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:input, :output) do
+ nil | nil
+ '' | ''
+ '[label](url)' | 'label(url)'
+ '<url|label>' | 'urllabel'
+ '<a href="url">label</a>' | 'a href="url"label/a'
+ end
+
+ with_them do
+ it 'returns the expected output' do
+ expect(described_class.sanitize(input)).to eq(output)
+ end
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 1fd2a92866d..f5fce559778 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -438,7 +438,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to the change password documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
+ is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
end
it 'mentions two factor authentication when two factor is not enabled' do
@@ -446,7 +446,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to two-factor authentication documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html'
+ is_expected.to have_body_text help_page_url('user/profile/account/two_factor_authentication')
end
context 'when two factor authentication is enabled' do
@@ -488,7 +488,7 @@ RSpec.describe Emails::Profile do
end
it 'includes a link to the change password documentation' do
- is_expected.to have_body_text 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password'
+ is_expected.to have_body_text help_page_url('user/profile/user_passwords', anchor: 'change-your-password')
end
end
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index e753bf2c76c..25afa8b48ce 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -9,11 +9,13 @@ RSpec.describe Emails::ServiceDesk do
include EmailHelpers
include_context 'gitlab email notification'
+ include_context 'with service desk mailer'
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:email) { 'someone@gitlab.com' }
+ let_it_be(:expected_unsubscribe_url) { unsubscribe_sent_notification_url('b7721fc7e8419911a8bea145236a0519') }
let(:template) { double(content: template_content) }
@@ -21,43 +23,6 @@ RSpec.describe Emails::ServiceDesk do
issue.issue_email_participants.create!(email: email)
end
- before do
- stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
-
- ServiceEmailClass.class_eval do
- include GitlabRoutingHelper
- include EmailsHelper
- include Emails::ServiceDesk
-
- helper GitlabRoutingHelper
- helper EmailsHelper
-
- # this method is implemented in Notify class, we don't need to test it
- def reply_key
- 'test-key'
- end
-
- # this method is implemented in Notify class, we don't need to test it
- def sender(author_id, params = {})
- author_id
- end
-
- # this method is implemented in Notify class
- #
- # We do not need to test the Notify method, it is already tested in notify_spec
- def mail_new_thread(issue, options)
- # we need to rewrite this in order to look up templates in the correct directory
- self.class.mailer_name = 'notify'
-
- # this is needed for default layout
- @unsubscribe_url = 'http://unsubscribe.example.com'
-
- mail(options)
- end
- alias_method :mail_answer_thread, :mail_new_thread
- end
- end
-
shared_examples 'handle template content' do |template_key, attachments_count|
before do
expect(Gitlab::Template::ServiceDeskTemplate).to receive(:find)
@@ -70,7 +35,7 @@ RSpec.describe Emails::ServiceDesk do
is_expected.to have_referable_subject(issue, include_project: false, reply: reply_in_subject)
is_expected.to have_body_text(expected_body)
expect(subject.attachments.count).to eq(attachments_count.to_i)
- expect(subject.content_type).to include('text/html')
+ expect(subject.content_type).to include(attachments_count.to_i > 0 ? 'multipart/mixed' : 'text/html')
end
end
end
@@ -134,13 +99,31 @@ RSpec.describe Emails::ServiceDesk do
it_behaves_like 'handle template content', 'thank_you'
end
- context 'with an issue id and issue path placeholders' do
- let(:template_content) { 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' }
- let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" }
+ context 'with an issue id, issue path and unsubscribe url placeholders' do
+ let(:template_content) do
+ 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' \
+ '[Unsubscribe](%{UNSUBSCRIBE_URL})'
+ end
+
+ let(:expected_body) do
+ "<p dir=\"auto\">thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" \
+ "<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
+ end
it_behaves_like 'handle template content', 'thank_you'
end
+ context 'with header and footer placeholders' do
+ let(:template_content) do
+ '%{SYSTEM_HEADER}' \
+ 'thank you, **your new issue** has been created.' \
+ '%{SYSTEM_FOOTER}'
+ end
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
+
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **your new issue:** %{ ISSUE_ID}' }
let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
@@ -174,13 +157,31 @@ RSpec.describe Emails::ServiceDesk do
it_behaves_like 'handle template content', 'new_note'
end
- context 'with an issue id, issue path and note placeholders' do
- let(:template_content) { 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' }
- let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" }
+ context 'with an issue id, issue path, note and unsubscribe url placeholders' do
+ let(:template_content) do
+ 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' \
+ '[Unsubscribe](%{UNSUBSCRIBE_URL})'
+ end
+
+ let(:expected_body) do
+ "<p dir=\"auto\">thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" \
+ "<a href=\"#{expected_unsubscribe_url}\">Unsubscribe</a></p>"
+ end
it_behaves_like 'handle template content', 'new_note'
end
+ context 'with header and footer placeholders' do
+ let(:template_content) do
+ '%{SYSTEM_HEADER}' \
+ 'thank you, **your new issue** has been created.' \
+ '%{SYSTEM_FOOTER}'
+ end
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
+
context 'with an issue id placeholder with whitespace' do
let(:template_content) { 'thank you, **new note on issue:** %{ ISSUE_ID}: %{ NOTE_TEXT }' }
let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{note.note}" }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 684fe9bb9cf..7f838e0caf9 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1401,6 +1401,7 @@ RSpec.describe Notify do
context 'for service desk issues' do
before do
+ stub_feature_flags(service_desk_custom_email: false)
issue.update!(external_author: 'service.desk@example.com')
issue.issue_email_participants.create!(email: 'service.desk@example.com')
end
@@ -1409,6 +1410,9 @@ RSpec.describe Notify do
subject { described_class.service_desk_thank_you_email(issue.id) }
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ it_behaves_like 'a mail with default delivery method'
it 'has the correct recipient' do
is_expected.to deliver_to('service.desk@example.com')
@@ -1442,6 +1446,41 @@ RSpec.describe Notify do
expect_sender(User.support_bot)
end
end
+
+ context 'when service_desk_custom_email is active' do
+ before do
+ stub_feature_flags(service_desk_custom_email: true)
+ end
+
+ it_behaves_like 'a mail with default delivery method'
+
+ it 'uses service bot name by default' do
+ expect_sender(User.support_bot)
+ end
+
+ context 'when custom email is enabled' do
+ let_it_be(:settings) do
+ create(
+ :service_desk_setting,
+ project: project,
+ custom_email_enabled: true,
+ custom_email: 'supersupport@example.com',
+ custom_email_smtp_address: 'smtp.example.com',
+ custom_email_smtp_port: 587,
+ custom_email_smtp_username: 'supersupport@example.com',
+ custom_email_smtp_password: 'supersecret'
+ )
+ end
+
+ it 'uses custom email and service bot name in "from" header' do
+ expect_sender(User.support_bot, sender_email: 'supersupport@example.com')
+ end
+
+ it 'uses SMTP delivery method and has correct settings' do
+ expect_service_desk_custom_email_delivery_options(settings)
+ end
+ end
+ end
end
describe 'new note email' do
@@ -1450,6 +1489,9 @@ RSpec.describe Notify do
subject { described_class.service_desk_new_note_email(issue.id, first_note.id, 'service.desk@example.com') }
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ it_behaves_like 'a mail with default delivery method'
it 'has the correct recipient' do
is_expected.to deliver_to('service.desk@example.com')
@@ -1465,6 +1507,41 @@ RSpec.describe Notify do
is_expected.to have_body_text(first_note.note)
end
end
+
+ context 'when service_desk_custom_email is active' do
+ before do
+ stub_feature_flags(service_desk_custom_email: true)
+ end
+
+ it_behaves_like 'a mail with default delivery method'
+
+ it 'uses author\'s name in "from" header' do
+ expect_sender(first_note.author)
+ end
+
+ context 'when custom email is enabled' do
+ let_it_be(:settings) do
+ create(
+ :service_desk_setting,
+ project: project,
+ custom_email_enabled: true,
+ custom_email: 'supersupport@example.com',
+ custom_email_smtp_address: 'smtp.example.com',
+ custom_email_smtp_port: 587,
+ custom_email_smtp_username: 'supersupport@example.com',
+ custom_email_smtp_password: 'supersecret'
+ )
+ end
+
+ it 'uses custom email and author\'s name in "from" header' do
+ expect_sender(first_note.author, sender_email: project.service_desk_setting.custom_email)
+ end
+
+ it 'uses SMTP delivery method and has correct settings' do
+ expect_service_desk_custom_email_delivery_options(settings)
+ end
+ end
+ end
end
end
end
@@ -2267,9 +2344,20 @@ RSpec.describe Notify do
end
end
- def expect_sender(user)
+ def expect_sender(user, sender_email: nil)
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq("#{user.name} (@#{user.username})")
- expect(sender.address).to eq(gitlab_sender)
+ expect(sender.address).to eq(sender_email.presence || gitlab_sender)
+ end
+
+ def expect_service_desk_custom_email_delivery_options(service_desk_setting)
+ expect(subject.delivery_method).to be_a Mail::SMTP
+ expect(subject.delivery_method.settings).to include(
+ address: service_desk_setting.custom_email_smtp_address,
+ port: service_desk_setting.custom_email_smtp_port,
+ user_name: service_desk_setting.custom_email_smtp_username,
+ password: service_desk_setting.custom_email_smtp_password,
+ domain: service_desk_setting.custom_email.split('@').last
+ )
end
end
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
deleted file mode 100644
index 706e0b14492..00000000000
--- a/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RemoveBuildsEmailServiceFromServices, feature_category: :navigation do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
- let(:namespace) { namespaces.create!(name: 'foo', path: 'bar') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
-
- it 'correctly deletes `BuildsEmailService` services' do
- services.create!(project_id: project.id, type: 'BuildsEmailService')
- services.create!(project_id: project.id, type: 'OtherService')
-
- expect(services.all.pluck(:type)).to match_array %w[BuildsEmailService OtherService]
-
- migrate!
-
- expect(services.all.pluck(:type)).to eq %w[OtherService]
- end
-end
diff --git a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb b/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
deleted file mode 100644
index 300c43b9133..00000000000
--- a/spec/migrations/20210610153556_delete_legacy_operations_feature_flags_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let(:operations_feature_flags) { table(:operations_feature_flags) }
- let(:operations_feature_flag_scopes) { table(:operations_feature_flag_scopes) }
- let(:operations_strategies) { table(:operations_strategies) }
- let(:operations_scopes) { table(:operations_scopes) }
- let(:operations_feature_flags_issues) { table(:operations_feature_flags_issues) }
-
- it 'correctly deletes legacy feature flags' do
- # Legacy version of a feature flag - dropped support in GitLab 14.0.
- legacy_flag = operations_feature_flags.create!(project_id: project.id, version: 1, name: 'flag_a', active: true, iid: 1)
- operations_feature_flag_scopes.create!(feature_flag_id: legacy_flag.id, active: true)
- operations_feature_flags_issues.create!(feature_flag_id: legacy_flag.id, issue_id: issue.id)
- # New version of a feature flag.
- new_flag = operations_feature_flags.create!(project_id: project.id, version: 2, name: 'flag_b', active: true, iid: 2)
- new_strategy = operations_strategies.create!(feature_flag_id: new_flag.id, name: 'default')
- operations_scopes.create!(strategy_id: new_strategy.id, environment_scope: '*')
- operations_feature_flags_issues.create!(feature_flag_id: new_flag.id, issue_id: issue.id)
-
- expect(operations_feature_flags.all.pluck(:version)).to contain_exactly(1, 2)
- expect(operations_feature_flag_scopes.count).to eq(1)
- expect(operations_strategies.count).to eq(1)
- expect(operations_scopes.count).to eq(1)
- expect(operations_feature_flags_issues.all.pluck(:feature_flag_id)).to contain_exactly(legacy_flag.id, new_flag.id)
-
- migrate!
-
- # Legacy flag is deleted.
- expect(operations_feature_flags.all.pluck(:version)).to contain_exactly(2)
- # The associated entries of the legacy flag are deleted too.
- expect(operations_feature_flag_scopes.count).to eq(0)
- # The associated entries of the new flag stay instact.
- expect(operations_strategies.count).to eq(1)
- expect(operations_scopes.count).to eq(1)
- expect(operations_feature_flags_issues.all.pluck(:feature_flag_id)).to contain_exactly(new_flag.id)
- end
-end
diff --git a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb b/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
deleted file mode 100644
index baa5fd7efbd..00000000000
--- a/spec/migrations/2021061716138_cascade_delete_freeze_periods_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
-
- describe "#up" do
- it 'allows for a project to be deleted' do
- freeze_periods.create!(id: 1, project_id: project.id, freeze_start: '5 * * * *', freeze_end: '6 * * * *', cron_timezone: 'UTC')
- migrate!
-
- project.delete
-
- expect(freeze_periods.where(project_id: project.id).count).to be_zero
- end
- end
-end
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
deleted file mode 100644
index 0f202129e82..00000000000
--- a/spec/migrations/20210708130419_reschedule_merge_request_diff_users_background_migration_spec.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RescheduleMergeRequestDiffUsersBackgroundMigration,
- :migration, feature_category: :code_review_workflow do
- let(:migration) { described_class.new }
-
- describe '#up' do
- before do
- allow(described_class::MergeRequestDiff)
- .to receive(:minimum)
- .with(:id)
- .and_return(42)
-
- allow(described_class::MergeRequestDiff)
- .to receive(:maximum)
- .with(:id)
- .and_return(85_123)
- end
-
- it 'deletes existing background migration job records' do
- args = [150_000, 300_000]
-
- Gitlab::Database::BackgroundMigrationJob
- .create!(class_name: described_class::MIGRATION_NAME, arguments: args)
-
- migration.up
-
- found = Gitlab::Database::BackgroundMigrationJob
- .where(class_name: described_class::MIGRATION_NAME, arguments: args)
- .count
-
- expect(found).to eq(0)
- end
-
- it 'schedules the migrations in batches' do
- expect(migration)
- .to receive(:migrate_in)
- .ordered
- .with(2.minutes.to_i, described_class::MIGRATION_NAME, [42, 40_042])
-
- expect(migration)
- .to receive(:migrate_in)
- .ordered
- .with(4.minutes.to_i, described_class::MIGRATION_NAME, [40_042, 80_042])
-
- expect(migration)
- .to receive(:migrate_in)
- .ordered
- .with(6.minutes.to_i, described_class::MIGRATION_NAME, [80_042, 120_042])
-
- migration.up
- end
-
- it 'creates rows to track the background migration jobs' do
- expect(Gitlab::Database::BackgroundMigrationJob)
- .to receive(:create!)
- .ordered
- .with(class_name: described_class::MIGRATION_NAME, arguments: [42, 40_042])
-
- expect(Gitlab::Database::BackgroundMigrationJob)
- .to receive(:create!)
- .ordered
- .with(class_name: described_class::MIGRATION_NAME, arguments: [40_042, 80_042])
-
- expect(Gitlab::Database::BackgroundMigrationJob)
- .to receive(:create!)
- .ordered
- .with(class_name: described_class::MIGRATION_NAME, arguments: [80_042, 120_042])
-
- migration.up
- end
- end
-end
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
deleted file mode 100644
index 6761b69aed5..00000000000
--- a/spec/migrations/20210713042000_fix_ci_sources_pipelines_index_names_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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
- expect(subject.foreign_key_exists?(:ci_sources_pipelines, :ci_pipelines, column: :pipeline_id, name: 'fk_e1bad85861')).to be_truthy
- expect(subject.foreign_key_exists?(:ci_sources_pipelines, :ci_pipelines, column: :source_pipeline_id, name: 'fk_d4e29af7d7')).to be_truthy
- expect(subject.foreign_key_exists?(:ci_sources_pipelines, :projects, column: :source_project_id, name: 'fk_acd9737679')).to be_truthy
- expect(subject.foreign_key_exists?(:ci_sources_pipelines, :projects, name: 'fk_1e53c97c0a')).to be_truthy
- expect(subject.foreign_key_exists?(:ci_sources_pipelines, :ci_builds, column: :source_job_id_convert_to_bigint, name: 'fk_be5624bf37_tmp')).to be_falsey
-
- expect(subject.index_exists_by_name?(:ci_sources_pipelines, described_class::NEW_INDEX_NAME)).to be_truthy
- expect(subject.index_exists_by_name?(:ci_sources_pipelines, described_class::OLD_INDEX_NAME)).to be_falsey
- end
- end
-
- it 'existing foreign keys and indexes are untouched' do
- validate_foreign_keys_and_index!
-
- migrate!
-
- validate_foreign_keys_and_index!
- end
-
- context 'with a legacy (pre-GitLab 10.0) foreign key' do
- let(:old_foreign_keys) { described_class::OLD_TO_NEW_FOREIGN_KEY_DEFS.keys }
- let(:new_foreign_keys) { described_class::OLD_TO_NEW_FOREIGN_KEY_DEFS.values.map { |entry| entry[:name] } }
-
- before do
- new_foreign_keys.each { |name| subject.remove_foreign_key_if_exists(:ci_sources_pipelines, name: name) }
-
- # GitLab 9.5.4: https://gitlab.com/gitlab-org/gitlab/-/blob/v9.5.4-ee/db/schema.rb#L2026-2030
- subject.add_foreign_key(:ci_sources_pipelines, :ci_builds, column: :source_job_id, name: 'fk_3f0c88d7dc', on_delete: :cascade)
- subject.add_foreign_key(:ci_sources_pipelines, :ci_pipelines, column: :pipeline_id, name: "fk_b8c0fac459", on_delete: :cascade)
- subject.add_foreign_key(:ci_sources_pipelines, :ci_pipelines, column: :source_pipeline_id, name: "fk_3a3e3cb83a", on_delete: :cascade)
- subject.add_foreign_key(:ci_sources_pipelines, :projects, column: :source_project_id, name: "fk_8868d0f3e4", on_delete: :cascade)
- subject.add_foreign_key(:ci_sources_pipelines, :projects, name: "fk_83b4346e48", on_delete: :cascade)
-
- # https://gitlab.com/gitlab-org/gitlab/-/blob/v9.5.4-ee/db/schema.rb#L443
- subject.add_index "ci_sources_pipelines", ["source_job_id"], name: described_class::OLD_INDEX_NAME, using: :btree
- end
-
- context 'when new index already exists' do
- it 'corrects foreign key constraints and drops old index' do
- expect { migrate! }.to change { subject.foreign_key_exists?(:ci_sources_pipelines, :ci_builds, column: :source_job_id, name: 'fk_3f0c88d7dc') }.from(true).to(false)
-
- validate_foreign_keys_and_index!
- end
- end
-
- context 'when new index does not exist' do
- before do
- subject.remove_index("ci_sources_pipelines", name: described_class::NEW_INDEX_NAME)
- end
-
- it 'drops the old index' do
- expect { migrate! }.to change { subject.index_exists_by_name?(:ci_sources_pipelines, described_class::OLD_INDEX_NAME) }.from(true).to(false)
-
- validate_foreign_keys_and_index!
- end
- end
- end
-end
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
deleted file mode 100644
index 5674efbf187..00000000000
--- a/spec/migrations/20210722042939_update_issuable_slas_where_issue_closed_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe UpdateIssuableSlasWhereIssueClosed, :migration, feature_category: :team_planning do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:issues) { table(:issues) }
- let(:issuable_slas) { table(:issuable_slas) }
- let(:issue_params) { { title: 'title', project_id: project.id } }
- let(:issue_closed_state) { 2 }
-
- let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
- let!(:project) { projects.create!(namespace_id: namespace.id) }
- let!(:issue_open) { issues.create!(issue_params) }
- let!(:issue_closed) { issues.create!(issue_params.merge(state_id: issue_closed_state)) }
-
- let!(:issuable_sla_open_issue) { issuable_slas.create!(issue_id: issue_open.id, due_at: Time.now) }
- let!(:issuable_sla_closed_issue) { issuable_slas.create!(issue_id: issue_closed.id, due_at: Time.now) }
-
- it 'sets the issuable_closed attribute to false' do
- expect(issuable_sla_open_issue.issuable_closed).to eq(false)
- expect(issuable_sla_closed_issue.issuable_closed).to eq(false)
-
- migrate!
-
- expect(issuable_sla_open_issue.reload.issuable_closed).to eq(false)
- expect(issuable_sla_closed_issue.reload.issuable_closed).to eq(true)
- end
-end
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
deleted file mode 100644
index 098dd647b27..00000000000
--- a/spec/migrations/20210722150102_operations_feature_flags_correct_flexible_rollout_values_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let(:feature_flag) { table(:operations_feature_flags).create!(project_id: project.id, active: true, name: 'foo', iid: 1) }
-
- describe "#up" do
- described_class::STICKINESS.each do |old, new|
- it "corrects parameters for flexible rollout stickiness #{old}" do
- reversible_migration do |migration|
- parameters = { groupId: "default", rollout: "100", stickiness: old }
- strategy = create_strategy(parameters)
-
- migration.before -> {
- expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => old })
- }
-
- migration.after -> {
- expect(strategy.reload.parameters).to eq({ "groupId" => "default", "rollout" => "100", "stickiness" => new })
- }
- end
- end
- end
-
- it 'ignores other strategies' do
- reversible_migration do |migration|
- parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "USERID" }
- strategy = create_strategy(parameters, name: 'default')
-
- migration.before -> {
- expect(strategy.reload.parameters).to eq(parameters)
- }
-
- migration.after -> {
- expect(strategy.reload.parameters).to eq(parameters)
- }
- end
- end
-
- it 'ignores other stickiness' do
- reversible_migration do |migration|
- parameters = { "groupId" => "default", "rollout" => "100", "stickiness" => "FOO" }
- strategy = create_strategy(parameters)
-
- migration.before -> {
- expect(strategy.reload.parameters).to eq(parameters)
- }
-
- migration.after -> {
- expect(strategy.reload.parameters).to eq(parameters)
- }
- end
- end
- end
-
- def create_strategy(params, name: 'flexibleRollout')
- strategies.create!(name: name, parameters: params, feature_flag_id: feature_flag.id)
- end
-end
diff --git a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb b/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
deleted file mode 100644
index e7f76eb0ae0..00000000000
--- a/spec/migrations/20210804150320_create_base_work_item_types_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CreateBaseWorkItemTypes, :migration, feature_category: :team_planning do
- include MigrationHelpers::WorkItemTypesHelper
-
- let!(:work_item_types) { table(:work_item_types) }
-
- let(:base_types) do
- {
- issue: 0,
- incident: 1,
- test_case: 2,
- requirement: 3
- }
- end
-
- # We use append_after to make sure this runs after the schema was reset to its latest state
- append_after(:all) do
- # Make sure base types are recreated after running the migration
- # because migration specs are not run in a transaction
- reset_work_item_types
- end
-
- it 'creates default data' do
- # Need to delete all as base types are seeded before entire test suite
- work_item_types.delete_all
-
- reversible_migration do |migration|
- migration.before -> {
- # Depending on whether the migration has been run before,
- # the size could be 4, or 0, so we don't set any expectations
- }
-
- migration.after -> {
- expect(work_item_types.count).to eq(4)
- expect(work_item_types.all.pluck(:base_type)).to match_array(base_types.values)
- }
- end
- end
-end
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
deleted file mode 100644
index d18673db757..00000000000
--- a/spec/migrations/20210805192450_update_trial_plans_ci_daily_pipeline_schedule_triggers_spec.rb
+++ /dev/null
@@ -1,137 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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') }
- let!(:ultimate_trial_plan) { plans.create!(name: 'ultimate_trial', title: 'Ultimate Trial') }
-
- describe '#up' do
- let!(:premium_trial_plan_limits) { plan_limits.create!(plan_id: premium_trial_plan.id, ci_daily_pipeline_schedule_triggers: 0) }
- let!(:ultimate_trial_plan_limits) { plan_limits.create!(plan_id: ultimate_trial_plan.id, ci_daily_pipeline_schedule_triggers: 0) }
-
- context 'when the environment is dev or com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'sets the trial plan limits for ci_daily_pipeline_schedule_triggers' do
- disable_migrations_output { migrate! }
-
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
-
- it 'does not change the plan limits if the ultimate trial plan is missing' do
- ultimate_trial_plan.destroy!
-
- expect { disable_migrations_output { migrate! } }.not_to change { plan_limits.count }
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
-
- it 'does not change the plan limits if the ultimate trial plan limits is missing' do
- ultimate_trial_plan_limits.destroy!
-
- expect { disable_migrations_output { migrate! } }.not_to change { plan_limits.count }
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
-
- it 'does not change the plan limits if the premium trial plan is missing' do
- premium_trial_plan.destroy!
-
- expect { disable_migrations_output { migrate! } }.not_to change { plan_limits.count }
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
-
- it 'does not change the plan limits if the premium trial plan limits is missing' do
- premium_trial_plan_limits.destroy!
-
- expect { disable_migrations_output { migrate! } }.not_to change { plan_limits.count }
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
- end
-
- context 'when the environment is anything other than dev or com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(false)
- end
-
- it 'does not update the plan limits' do
- disable_migrations_output { migrate! }
-
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
- end
- end
-
- describe '#down' do
- let!(:premium_trial_plan_limits) { plan_limits.create!(plan_id: premium_trial_plan.id, ci_daily_pipeline_schedule_triggers: 288) }
- let!(:ultimate_trial_plan_limits) { plan_limits.create!(plan_id: ultimate_trial_plan.id, ci_daily_pipeline_schedule_triggers: 288) }
-
- context 'when the environment is dev or com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'sets the trial plan limits ci_daily_pipeline_schedule_triggers to zero' do
- migrate_down!
-
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(0)
- end
-
- it 'does not change the plan limits if the ultimate trial plan is missing' do
- ultimate_trial_plan.destroy!
-
- expect { migrate_down! }.not_to change { plan_limits.count }
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
-
- it 'does not change the plan limits if the ultimate trial plan limits is missing' do
- ultimate_trial_plan_limits.destroy!
-
- expect { migrate_down! }.not_to change { plan_limits.count }
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
-
- it 'does not change the plan limits if the premium trial plan is missing' do
- premium_trial_plan.destroy!
-
- expect { migrate_down! }.not_to change { plan_limits.count }
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
-
- it 'does not change the plan limits if the premium trial plan limits is missing' do
- premium_trial_plan_limits.destroy!
-
- expect { migrate_down! }.not_to change { plan_limits.count }
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
- end
-
- context 'when the environment is anything other than dev or com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(false)
- end
-
- it 'does not change the ultimate trial plan limits' do
- migrate_down!
-
- expect(ultimate_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- expect(premium_trial_plan_limits.reload.ci_daily_pipeline_schedule_triggers).to eq(288)
- end
- end
- end
-
- def migrate_down!
- disable_migrations_output do
- migrate!
- described_class.new.down
- end
- end
-end
diff --git a/spec/migrations/20210811122206_update_external_project_bots_spec.rb b/spec/migrations/20210811122206_update_external_project_bots_spec.rb
deleted file mode 100644
index aa0bce63865..00000000000
--- a/spec/migrations/20210811122206_update_external_project_bots_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe UpdateExternalProjectBots, :migration, feature_category: :users do
- def create_user(**extra_options)
- defaults = { projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
-
- table(:users).create!(defaults.merge(extra_options))
- end
-
- it 'sets bot users as external if were created by external users' do
- internal_user = create_user(username: 'foo')
- external_user = create_user(username: 'bar', external: true)
-
- internal_project_bot = create_user(username: 'foo2', user_type: 6, created_by_id: internal_user.id, external: false)
- external_project_bot = create_user(username: 'bar2', user_type: 6, created_by_id: external_user.id, external: false)
-
- migrate!
-
- expect(table(:users).find(internal_project_bot.id).external).to eq false
- expect(table(:users).find(external_project_bot.id).external).to eq true
- end
-end
diff --git a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb b/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
deleted file mode 100644
index fcc2e1657d0..00000000000
--- a/spec/migrations/20210812013042_remove_duplicate_project_authorizations_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!('remove_duplicate_project_authorizations')
-
-RSpec.describe RemoveDuplicateProjectAuthorizations, :migration, feature_category: :authentication_and_authorization do
- let(:users) { table(:users) }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_authorizations) { table(:project_authorizations) }
-
- let!(:user_1) { users.create! email: 'user1@example.com', projects_limit: 0 }
- let!(:user_2) { users.create! email: 'user2@example.com', projects_limit: 0 }
- let!(:namespace_1) { namespaces.create! name: 'namespace 1', path: 'namespace1' }
- let!(:namespace_2) { namespaces.create! name: 'namespace 2', path: 'namespace2' }
- let!(:project_1) { projects.create! namespace_id: namespace_1.id }
- let!(:project_2) { projects.create! namespace_id: namespace_2.id }
-
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
- end
-
- describe '#up' do
- subject { migrate! }
-
- context 'User with multiple projects' do
- before do
- project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::DEVELOPER
- project_authorizations.create! project_id: project_2.id, user_id: user_1.id, access_level: Gitlab::Access::DEVELOPER
- end
-
- it { expect { subject }.not_to change { ProjectAuthorization.count } }
- end
-
- context 'Project with multiple users' do
- before do
- project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::DEVELOPER
- project_authorizations.create! project_id: project_1.id, user_id: user_2.id, access_level: Gitlab::Access::DEVELOPER
- end
-
- it { expect { subject }.not_to change { ProjectAuthorization.count } }
- end
-
- context 'Same project and user but different access level' do
- before do
- project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::DEVELOPER
- project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::MAINTAINER
- project_authorizations.create! project_id: project_1.id, user_id: user_1.id, access_level: Gitlab::Access::REPORTER
- end
-
- it { expect { subject }.to change { ProjectAuthorization.count }.from(3).to(1) }
-
- it 'retains the highest access level' do
- subject
-
- all_records = ProjectAuthorization.all.to_a
- expect(all_records.count).to eq 1
- expect(all_records.first.access_level).to eq Gitlab::Access::MAINTAINER
- end
- end
- end
-end
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
deleted file mode 100644
index e48f933ad5f..00000000000
--- a/spec/migrations/20210819145000_drop_temporary_columns_and_triggers_for_ci_builds_runner_session_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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
- reversible_migration do |migration|
- migration.before -> {
- expect(ci_builds_runner_session_table.column_names).to include('build_id_convert_to_bigint')
- }
-
- migration.after -> {
- ci_builds_runner_session_table.reset_column_information
- expect(ci_builds_runner_session_table.column_names).not_to include('build_id_convert_to_bigint')
- }
- end
- end
-end
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 a9a814f9a48..a198ae9e473 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, feature_category: :users do
+RSpec.describe CleanupOrphanProjectAccessTokens, :migration, feature_category: :user_profile do
def create_user(**extra_options)
defaults = { state: 'active', projects_limit: 0, email: "#{extra_options[:username]}@example.com" }
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 f615c8bb50e..49cf1a01f2a 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, feature_category: :users do
+RSpec.describe DropInt4ColumnForEvents, feature_category: :user_profile 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 5c39e7530ff..3e241438339 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, feature_category: :users do
+RSpec.describe DropInt4ColumnForPushEventPayloads, feature_category: :user_profile do
let(:push_event_payloads) { table(:push_event_payloads) }
it 'correctly migrates up and down' do
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 a57d3633ecf..c0b94313d4d 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, feature_category: :sharding do
+RSpec.describe RemoveLeftoverExternalPullRequestDeletions, feature_category: :pods 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/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb b/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
index 555856788b7..e9bca42f37f 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, feature_category: :sharding do
+RSpec.describe RemoveLeftoverCiJobArtifactDeletions, feature_category: :pods do
let(:deleted_records) { table(:loose_foreign_keys_deleted_records) }
target_table_name = Ci::JobArtifact.table_name
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 dd77ce503b8..36c65612bb9 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, feature_category: :utilization do
+RSpec.describe RemoveDeactivatedUserHighestRoleStats, feature_category: :subscription_cost_management do
let!(:users) { table(:users) }
let!(:user_highest_roles) { table(:user_highest_roles) }
diff --git a/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb b/spec/migrations/20221008032350_add_password_expiration_migration_spec.rb
index 1663966816c..ee6c2aeca9c 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, feature_category: :users do
+RSpec.describe AddPasswordExpirationMigration, feature_category: :user_profile 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 e2c508938fd..5c228381b57 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,7 +4,7 @@ require 'spec_helper'
require_migration!
-RSpec.describe AddPasswordLastChangedAtToUserDetails, feature_category: :users do
+RSpec.describe AddPasswordLastChangedAtToUserDetails, feature_category: :user_profile do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:users) { table(:users) }
let!(:user) { create_user! }
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 0686d9ca786..ad644b63060 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, feature_category: :users do
+RSpec.describe UpdateInvalidDormantUserSetting, :migration, feature_category: :user_profile do
let(:settings) { table(:application_settings) }
context 'with no rows in the application_settings table' 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 d6acf31fdc6..0e5bb419e32 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, feature_category: :users do
+RSpec.describe ChangeDefaultValueOnPasswordLastChangedAtToUserDetails, :migration, feature_category: :user_profile 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 6b6fb553b1f..332b3a5abba 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, feature_category: :users do
+RSpec.describe AddIndexOnPasswordLastChangedAtToUserDetails, :migration, feature_category: :user_profile do
let(:index_name) { 'index_user_details_on_password_last_changed_at' }
it 'correctly migrates up and down' do
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
index 5a5bc42a37b..9eaab56de7c 100644
--- a/spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb
+++ b/spec/migrations/20221122132812_schedule_prune_stale_project_export_jobs_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe SchedulePruneStaleProjectExportJobs, category: :importers do
+RSpec.describe SchedulePruneStaleProjectExportJobs, feature_category: :importers do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
diff --git a/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb b/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb
index b76f889d743..e03fd9c9daf 100644
--- a/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb
+++ b/spec/migrations/20221205151917_schedule_backfill_environment_tier_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleBackfillEnvironmentTier, category: :continuous_delivery do
+RSpec.describe ScheduleBackfillEnvironmentTier, feature_category: :continuous_delivery do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
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
index 899074399a1..d8270816afe 100644
--- a/spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb
+++ b/spec/migrations/20221209110934_update_import_sources_on_application_settings_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe UpdateImportSourcesOnApplicationSettings, feature_category: :migration do
+RSpec.describe UpdateImportSourcesOnApplicationSettings, feature_category: :importers 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] }
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
index e5b20b2d48a..1f276109b24 100644
--- 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
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe FixUpdateImportSourcesOnApplicationSettings, feature_category: :migration do
+RSpec.describe FixUpdateImportSourcesOnApplicationSettings, feature_category: :importers do
let(:settings) { table(:application_settings) }
let(:import_sources) { %w[github git bitbucket bitbucket_server] }
diff --git a/spec/migrations/20221219122320_copy_clickhouse_connection_string_to_encrypted_var_spec.rb b/spec/migrations/20221219122320_copy_clickhouse_connection_string_to_encrypted_var_spec.rb
new file mode 100644
index 00000000000..7ff033ab0c2
--- /dev/null
+++ b/spec/migrations/20221219122320_copy_clickhouse_connection_string_to_encrypted_var_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CopyClickhouseConnectionStringToEncryptedVar, feature_category: :product_analytics do
+ let!(:migration) { described_class.new }
+ let(:setting) { table(:application_settings).create!(clickhouse_connection_string: 'https://example.com/test') }
+
+ it 'copies the clickhouse_connection_string to the encrypted column' do
+ expect(setting.clickhouse_connection_string).to eq('https://example.com/test')
+
+ migrate!
+
+ setting.reload
+ expect(setting.clickhouse_connection_string).to eq('https://example.com/test')
+ expect(setting.encrypted_product_analytics_clickhouse_connection_string).not_to be_nil
+ end
+end
diff --git a/spec/migrations/20221226153252_queue_fix_incoherent_packages_size_on_project_statistics_spec.rb b/spec/migrations/20221226153252_queue_fix_incoherent_packages_size_on_project_statistics_spec.rb
new file mode 100644
index 00000000000..b375546b90e
--- /dev/null
+++ b/spec/migrations/20221226153252_queue_fix_incoherent_packages_size_on_project_statistics_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueFixIncoherentPackagesSizeOnProjectStatistics, feature_category: :package_registry do
+ let(:batched_migration) { described_class::MIGRATION }
+
+ context 'with no packages' do
+ 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
+
+ context 'with some packages' do
+ before do
+ namespace = table(:namespaces)
+ .create!(name: 'project', path: 'project', type: 'Project')
+ project = table(:projects).create!(
+ name: 'project',
+ path: 'project',
+ project_namespace_id: namespace.id,
+ namespace_id: namespace.id
+ )
+ table(:packages_packages)
+ .create!(name: 'test', version: '1.2.3', package_type: 2, project_id: project.id)
+ 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: :project_statistics,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE
+ )
+ }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb b/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb
new file mode 100644
index 00000000000..5c572b49d3d
--- /dev/null
+++ b/spec/migrations/20230117114739_clear_duplicate_jobs_cookies_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ClearDuplicateJobsCookies, :migration, feature_category: :redis do
+ def with_redis(&block)
+ Gitlab::Redis::Queues.with(&block)
+ end
+
+ it 'deletes duplicate jobs cookies' do
+ delete = ['resque:gitlab:duplicate:blabla:1:cookie:v2', 'resque:gitlab:duplicate:foobar:2:cookie:v2']
+ keep = ['resque:gitlab:duplicate:something', 'something:cookie:v2']
+ with_redis { |r| (delete + keep).each { |key| r.set(key, 'value') } }
+
+ expect(with_redis { |r| r.exists(delete + keep) }).to eq(4)
+
+ migrate!
+
+ expect(with_redis { |r| r.exists(delete) }).to eq(0)
+ expect(with_redis { |r| r.exists(keep) }).to eq(2)
+ end
+end
diff --git a/spec/migrations/20230125093723_rebalance_partition_id_ci_pipeline_spec.rb b/spec/migrations/20230125093723_rebalance_partition_id_ci_pipeline_spec.rb
new file mode 100644
index 00000000000..3ccd92e15a4
--- /dev/null
+++ b/spec/migrations/20230125093723_rebalance_partition_id_ci_pipeline_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RebalancePartitionIdCiPipeline, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_builds' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_pipelines,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230125093840_rebalance_partition_id_ci_build_spec.rb b/spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb
new file mode 100644
index 00000000000..b983564a2d9
--- /dev/null
+++ b/spec/migrations/20230125093840_rebalance_partition_id_ci_build_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RebalancePartitionIdCiBuild, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_builds' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_builds,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230130073109_nullify_creator_id_of_orphaned_projects_spec.rb b/spec/migrations/20230130073109_nullify_creator_id_of_orphaned_projects_spec.rb
new file mode 100644
index 00000000000..9d4d50fab54
--- /dev/null
+++ b/spec/migrations/20230130073109_nullify_creator_id_of_orphaned_projects_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe NullifyCreatorIdOfOrphanedProjects, feature_category: :projects do
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background migration' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :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 'removes scheduled background migrations' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/20230131125844_add_project_id_name_id_version_index_to_installable_npm_packages_spec.rb b/spec/migrations/20230131125844_add_project_id_name_id_version_index_to_installable_npm_packages_spec.rb
new file mode 100644
index 00000000000..5d8c7ab4745
--- /dev/null
+++ b/spec/migrations/20230131125844_add_project_id_name_id_version_index_to_installable_npm_packages_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddProjectIdNameIdVersionIndexToInstallableNpmPackages, feature_category: :package_registry do
+ it 'schedules an index creation' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(ActiveRecord::Base.connection.indexes('packages_packages').map(&:name))
+ .not_to include('idx_packages_on_project_id_name_id_version_when_installable_npm')
+ }
+
+ migration.after -> {
+ expect(ActiveRecord::Base.connection.indexes('packages_packages').map(&:name))
+ .to include('idx_packages_on_project_id_name_id_version_when_installable_npm')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20230201171450_finalize_backfill_environment_tier_migration_spec.rb b/spec/migrations/20230201171450_finalize_backfill_environment_tier_migration_spec.rb
new file mode 100644
index 00000000000..3fc9c7d8af7
--- /dev/null
+++ b/spec/migrations/20230201171450_finalize_backfill_environment_tier_migration_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeBackfillEnvironmentTierMigration, :migration, feature_category: :continuous_delivery 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('BackfillEnvironmentTiers', :environments, :id, [])
+ end
+ end
+ end
+
+ 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:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:group_member_namespace_id_backfill) do
+ batched_migrations.create!(
+ job_class_name: 'BackfillEnvironmentTiers',
+ table_name: :environments,
+ 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 migration finished successfully' do
+ it 'does not raise exception' 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
+ before do
+ group_member_namespace_id_backfill.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230202131928_encrypt_ci_trigger_token_spec.rb b/spec/migrations/20230202131928_encrypt_ci_trigger_token_spec.rb
new file mode 100644
index 00000000000..a8896e7d3cf
--- /dev/null
+++ b/spec/migrations/20230202131928_encrypt_ci_trigger_token_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EncryptCiTriggerToken, migration: :gitlab_ci, feature_category: :continuous_integration 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('EncryptCiTriggerToken', :ci_triggers, :id, [])
+ end
+ end
+ end
+
+ context 'with migration present' do
+ let!(:ci_trigger_token_encryption_migration) do
+ batched_migrations.create!(
+ job_class_name: 'EncryptCiTriggerToken',
+ table_name: :ci_triggers,
+ column_name: :token,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 100,
+ gitlab_schema: :gitlab_ci,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+
+ it 'schedules background jobs for each batch of ci_triggers' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_triggers,
+ column_name: :token,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ 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
+ before do
+ ci_trigger_token_encryption_migration.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ 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/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb b/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb
new file mode 100644
index 00000000000..26c63e6deb2
--- /dev/null
+++ b/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleVulnerabilitiesFeedbackMigration4, 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::JOB_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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/20230208100917_fix_partition_ids_for_ci_pipeline_variable_spec.rb b/spec/migrations/20230208100917_fix_partition_ids_for_ci_pipeline_variable_spec.rb
new file mode 100644
index 00000000000..fb0e1fe17ec
--- /dev/null
+++ b/spec/migrations/20230208100917_fix_partition_ids_for_ci_pipeline_variable_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiPipelineVariable, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_pipeline_variables' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_pipeline_variables,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230208103009_fix_partition_ids_for_ci_job_artifact_spec.rb b/spec/migrations/20230208103009_fix_partition_ids_for_ci_job_artifact_spec.rb
new file mode 100644
index 00000000000..de2386c6a0d
--- /dev/null
+++ b/spec/migrations/20230208103009_fix_partition_ids_for_ci_job_artifact_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiJobArtifact, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_job_artifacts' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_job_artifacts,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230208132608_fix_partition_ids_for_ci_stage_spec.rb b/spec/migrations/20230208132608_fix_partition_ids_for_ci_stage_spec.rb
new file mode 100644
index 00000000000..8b057afc1e9
--- /dev/null
+++ b/spec/migrations/20230208132608_fix_partition_ids_for_ci_stage_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiStage, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_stages' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_stages,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230209090702_fix_partition_ids_for_ci_build_report_result_spec.rb b/spec/migrations/20230209090702_fix_partition_ids_for_ci_build_report_result_spec.rb
new file mode 100644
index 00000000000..f0ac8239f58
--- /dev/null
+++ b/spec/migrations/20230209090702_fix_partition_ids_for_ci_build_report_result_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiBuildReportResult,
+ migration: :gitlab_ci,
+ feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_build_report_results' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_build_report_results,
+ column_name: :build_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230209092204_fix_partition_ids_for_ci_build_trace_metadata_spec.rb b/spec/migrations/20230209092204_fix_partition_ids_for_ci_build_trace_metadata_spec.rb
new file mode 100644
index 00000000000..a93ba36d9ae
--- /dev/null
+++ b/spec/migrations/20230209092204_fix_partition_ids_for_ci_build_trace_metadata_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiBuildTraceMetadata,
+ migration: :gitlab_ci,
+ feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of ci_build_trace_metadata' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :ci_build_trace_metadata,
+ column_name: :build_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230209140102_fix_partition_ids_for_ci_build_metadata_spec.rb b/spec/migrations/20230209140102_fix_partition_ids_for_ci_build_metadata_spec.rb
new file mode 100644
index 00000000000..c354d68749f
--- /dev/null
+++ b/spec/migrations/20230209140102_fix_partition_ids_for_ci_build_metadata_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiBuildMetadata,
+ migration: :gitlab_ci,
+ feature_category: :continuous_integration do
+ let(:migration) { described_class::MIGRATION }
+
+ context 'when on saas' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of p_ci_builds_metadata' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ gitlab_schema: :gitlab_ci,
+ table_name: :p_ci_builds_metadata,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_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
+
+ context 'when on self-managed instance' do
+ let(:migration) { described_class.new }
+
+ 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/20230214122717_fix_partition_ids_for_ci_job_variables_spec.rb b/spec/migrations/20230214122717_fix_partition_ids_for_ci_job_variables_spec.rb
new file mode 100644
index 00000000000..64275855262
--- /dev/null
+++ b/spec/migrations/20230214122717_fix_partition_ids_for_ci_job_variables_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsForCiJobVariables, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:builds) { table(:ci_builds, database: :ci) }
+ let(:job_variables) { table(:ci_job_variables, database: :ci) }
+ let(:connection) { job_variables.connection }
+
+ around do |example|
+ connection.execute "ALTER TABLE #{job_variables.quoted_table_name} DISABLE TRIGGER ALL;"
+
+ example.run
+ ensure
+ connection.execute "ALTER TABLE #{job_variables.quoted_table_name} ENABLE TRIGGER ALL;"
+ end
+
+ before do
+ job = builds.create!(partition_id: 100)
+
+ job_variables.insert_all!([
+ { job_id: job.id, partition_id: 100, key: 'variable-100' },
+ { job_id: job.id, partition_id: 101, key: 'variable-101' }
+ ])
+ end
+
+ describe '#up', :aggregate_failures do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'fixes partition_id' do
+ expect { migrate! }.not_to raise_error
+
+ expect(job_variables.where(partition_id: 100).count).to eq(2)
+ expect(job_variables.where(partition_id: 101).count).to eq(0)
+ end
+ end
+
+ context 'when on self managed' do
+ it 'does not change partition_id' do
+ expect { migrate! }.not_to raise_error
+
+ expect(job_variables.where(partition_id: 100).count).to eq(1)
+ expect(job_variables.where(partition_id: 101).count).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20230214154101_fix_partition_ids_on_ci_sources_pipelines_spec.rb b/spec/migrations/20230214154101_fix_partition_ids_on_ci_sources_pipelines_spec.rb
new file mode 100644
index 00000000000..44031175497
--- /dev/null
+++ b/spec/migrations/20230214154101_fix_partition_ids_on_ci_sources_pipelines_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixPartitionIdsOnCiSourcesPipelines, migration: :gitlab_ci, feature_category: :continuous_integration do
+ let(:sources_pipelines) { table(:ci_sources_pipelines, database: :ci) }
+
+ before do
+ sources_pipelines.insert_all!([
+ { partition_id: 100, source_partition_id: 100 },
+ { partition_id: 100, source_partition_id: 101 },
+ { partition_id: 101, source_partition_id: 100 },
+ { partition_id: 101, source_partition_id: 101 }
+ ])
+ end
+
+ describe '#up' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'fixes partition_id and source_partition_id' do
+ expect { migrate! }.not_to raise_error
+
+ expect(sources_pipelines.where(partition_id: 100).count).to eq(4)
+ expect(sources_pipelines.where(partition_id: 101).count).to eq(0)
+ expect(sources_pipelines.where(source_partition_id: 100).count).to eq(4)
+ expect(sources_pipelines.where(source_partition_id: 101).count).to eq(0)
+ end
+ end
+
+ context 'when on self managed' do
+ it 'does not change partition_id or source_partition_id' do
+ expect { migrate! }.not_to raise_error
+
+ expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
+ expect(sources_pipelines.where(partition_id: 100).count).to eq(2)
+ expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
+ expect(sources_pipelines.where(source_partition_id: 101).count).to eq(2)
+ end
+ end
+ end
+end
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
deleted file mode 100644
index a6c892db131..00000000000
--- a/spec/migrations/add_default_project_approval_rules_vuln_allowed_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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') }
- let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
- let(:approval_project_rules) { table(:approval_project_rules) }
-
- it 'updates records when vulnerabilities_allowed is nil' do
- records_to_migrate = 10
-
- records_to_migrate.times do |i|
- approval_project_rules.create!(name: "rule #{i}", project_id: project.id)
- end
-
- expect { migrate! }
- .to change { approval_project_rules.where(vulnerabilities_allowed: nil).count }
- .from(records_to_migrate)
- .to(0)
- end
-
- it 'defaults vulnerabilities_allowed to 0' do
- approval_project_rule = approval_project_rules.create!(name: "new rule", project_id: project.id)
-
- expect(approval_project_rule.vulnerabilities_allowed).to be_nil
-
- migrate!
-
- expect(approval_project_rule.reload.vulnerabilities_allowed).to eq(0)
- end
-end
diff --git a/spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb b/spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb
new file mode 100644
index 00000000000..6cab3ca3d8f
--- /dev/null
+++ b/spec/migrations/add_namespaces_emails_enabled_column_data_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+require_migration!
+
+RSpec.describe AddNamespacesEmailsEnabledColumnData, :migration, feature_category: :database do
+ before :all do
+ Rake.application.rake_require 'active_record/railties/databases'
+ Rake.application.rake_require 'tasks/gitlab/db'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ let(:migration) { described_class::MIGRATION }
+ let(:projects) { table(:projects) }
+ let(:namespace_settings_table) { table(:namespace_settings) }
+ let(:namespaces) { table(:namespaces) }
+
+ before do
+ stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2)
+ end
+
+ it 'schedules background migrations', :aggregate_failures do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :namespaces,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ 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
+
+ it 'sets emails_enabled to be the opposite of emails_disabled' do
+ disabled_records_to_migrate = 6
+ enabled_records_to_migrate = 4
+
+ disabled_records_to_migrate.times do |i|
+ namespace = namespaces.create!(name: 'namespace',
+ path: "namespace#{i}",
+ emails_disabled: true)
+ namespace_settings_table.create!(namespace_id: namespace.id)
+ end
+
+ enabled_records_to_migrate.times do |i|
+ namespace = namespaces.create!(name: 'namespace',
+ path: "namespace#{i}",
+ emails_disabled: false)
+ namespace_settings_table.create!(namespace_id: namespace.id)
+ end
+
+ migrate!
+ run_rake_task('gitlab:db:execute_batched_migrations')
+ # rubocop: disable CodeReuse/ActiveRecord
+ expect(namespace_settings_table.where(emails_enabled: true).count).to eq(enabled_records_to_migrate)
+ expect(namespace_settings_table.where(emails_enabled: false).count).to eq(disabled_records_to_migrate)
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb b/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
deleted file mode 100644
index 670541128a0..00000000000
--- a/spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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 {
- [
- AddPremiumAndUltimatePlanLimits::Plan.count,
- AddPremiumAndUltimatePlanLimits::PlanLimits.count
- ]
- }
- end
- end
-
- describe '#up' do
- context 'when not .com?' do
- before do
- allow(Gitlab).to receive(:com?).and_return false
- end
-
- it_behaves_like 'a migration that does not alter plans or plan limits'
- end
-
- context 'when .com?' do
- before do
- allow(Gitlab).to receive(:com?).and_return true
- end
-
- context 'when source plan does not exist' do
- it_behaves_like 'a migration that does not alter plans or plan limits'
- end
-
- context 'when target plan does not exist' do
- before do
- table(:plans).create!(name: 'silver', title: 'Silver')
- table(:plans).create!(name: 'gold', title: 'Gold')
- end
-
- it_behaves_like 'a migration that does not alter plans or plan limits'
- end
-
- context 'when source and target plans exist' do
- let!(:silver) { table(:plans).create!(name: 'silver', title: 'Silver') }
- let!(:gold) { table(:plans).create!(name: 'gold', title: 'Gold') }
- let!(:premium) { table(:plans).create!(name: 'premium', title: 'Premium') }
- let!(:ultimate) { table(:plans).create!(name: 'ultimate', title: 'Ultimate') }
-
- let!(:silver_limits) { table(:plan_limits).create!(plan_id: silver.id, storage_size_limit: 111) }
- let!(:gold_limits) { table(:plan_limits).create!(plan_id: gold.id, storage_size_limit: 222) }
-
- context 'when target has plan limits' do
- before do
- table(:plan_limits).create!(plan_id: premium.id, storage_size_limit: 999)
- table(:plan_limits).create!(plan_id: ultimate.id, storage_size_limit: 999)
- end
-
- it 'does not overwrite the limits' do
- expect { migrate! }.not_to change {
- [
- AddPremiumAndUltimatePlanLimits::Plan.count,
- AddPremiumAndUltimatePlanLimits::PlanLimits.pluck(:id, :storage_size_limit).sort
- ]
- }
- end
- end
-
- context 'when target has no plan limits' do
- it 'creates plan limits from the source plan' do
- migrate!
-
- expect(AddPremiumAndUltimatePlanLimits::PlanLimits.pluck(:plan_id, :storage_size_limit))
- .to match_array(
- [
- [silver.id, silver_limits.storage_size_limit],
- [gold.id, gold_limits.storage_size_limit],
- [premium.id, silver_limits.storage_size_limit],
- [ultimate.id, gold_limits.storage_size_limit]
- ])
- end
- end
- end
- end
- end
-end
diff --git a/spec/migrations/add_projects_emails_enabled_column_data_spec.rb b/spec/migrations/add_projects_emails_enabled_column_data_spec.rb
new file mode 100644
index 00000000000..1d021ecd439
--- /dev/null
+++ b/spec/migrations/add_projects_emails_enabled_column_data_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+require_migration!
+
+RSpec.describe AddProjectsEmailsEnabledColumnData, :migration, feature_category: :database do
+ before :all do
+ Rake.application.rake_require 'active_record/railties/databases'
+ Rake.application.rake_require 'tasks/gitlab/db'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ let(:migration) { described_class::MIGRATION }
+ let(:project_settings) { table(:project_settings) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+
+ before do
+ stub_const("#{described_class.name}::SUB_BATCH_SIZE", 2)
+ end
+
+ it 'schedules background migrations', :aggregate_failures do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ 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
+
+ it 'sets emails_enabled to be the opposite of emails_disabled' do
+ disabled_records_to_migrate = 4
+ enabled_records_to_migrate = 2
+
+ disabled_records_to_migrate.times do |i|
+ namespace = namespaces.create!(name: 'namespace', path: "namespace#{i}")
+ project = projects.create!(name: "Project Disabled #{i}",
+ path: "projectDisabled#{i}",
+ namespace_id: namespace.id,
+ project_namespace_id: namespace.id,
+ emails_disabled: true)
+ project_settings.create!(project_id: project.id)
+ end
+
+ enabled_records_to_migrate.times do |i|
+ namespace = namespaces.create!(name: 'namespace', path: "namespace#{i}")
+ project = projects.create!(name: "Project Enabled #{i}",
+ path: "projectEnabled#{i}",
+ namespace_id: namespace.id,
+ project_namespace_id: namespace.id,
+ emails_disabled: false)
+ project_settings.create!(project_id: project.id)
+ end
+
+ migrate!
+ run_rake_task('gitlab:db:execute_batched_migrations')
+ # rubocop: disable CodeReuse/ActiveRecord
+ expect(project_settings.where(emails_enabled: true).count).to eq(enabled_records_to_migrate)
+ expect(project_settings.where(emails_enabled: false).count).to eq(disabled_records_to_migrate)
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/spec/migrations/add_triggers_to_integrations_type_new_spec.rb b/spec/migrations/add_triggers_to_integrations_type_new_spec.rb
deleted file mode 100644
index 4fa5fe31d2b..00000000000
--- a/spec/migrations/add_triggers_to_integrations_type_new_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe AddTriggersToIntegrationsTypeNew, feature_category: :purchase do
- let(:migration) { described_class.new }
- let(:integrations) { table(:integrations) }
-
- # This matches Gitlab::Integrations::StiType at the time the trigger was added
- let(:namespaced_integrations) do
- %w[
- Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
- Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Irker Jenkins Jira Mattermost
- MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker
- Prometheus Pushover Redmine Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack
-
- Github GitlabSlackApplication
- ]
- end
-
- describe '#up' do
- before do
- migrate!
- end
-
- describe 'INSERT trigger' do
- it 'sets `type_new` to the transformed `type` class name' do
- namespaced_integrations.each do |type|
- integration = integrations.create!(type: "#{type}Service")
-
- expect(integration.reload).to have_attributes(
- type: "#{type}Service",
- type_new: "Integrations::#{type}"
- )
- end
- end
-
- it 'ignores types that are not namespaced' do
- # We don't actually have any integrations without namespaces,
- # but we can abuse one of the integration base classes.
- integration = integrations.create!(type: 'BaseIssueTracker')
-
- expect(integration.reload).to have_attributes(
- type: 'BaseIssueTracker',
- type_new: nil
- )
- end
-
- it 'ignores types that are unknown' do
- integration = integrations.create!(type: 'FooBar')
-
- expect(integration.reload).to have_attributes(
- type: 'FooBar',
- type_new: nil
- )
- end
- end
- end
-
- describe '#down' do
- before do
- migration.up
- migration.down
- end
-
- it 'drops the INSERT trigger' do
- integration = integrations.create!(type: 'JiraService')
-
- expect(integration.reload).to have_attributes(
- type: 'JiraService',
- type_new: nil
- )
- end
- end
-end
diff --git a/spec/migrations/add_upvotes_count_index_to_issues_spec.rb b/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
deleted file mode 100644
index 0012b8a0b96..00000000000
--- a/spec/migrations/add_upvotes_count_index_to_issues_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AddUpvotesCountIndexToIssues, feature_category: :team_planning do
- let(:migration_instance) { described_class.new }
-
- describe '#up' do
- it 'adds index' do
- expect { migrate! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(false).to(true)
- end
- end
-
- describe '#down' do
- it 'removes index' do
- migrate!
-
- expect { schema_migrate_down! }.to change { migration_instance.index_exists?(:issues, [:project_id, :upvotes_count], name: described_class::INDEX_NAME) }.from(true).to(false)
- end
- end
-end
diff --git a/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb b/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb
deleted file mode 100644
index 67d215c781b..00000000000
--- a/spec/migrations/associate_existing_dast_builds_with_variables_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe AssociateExistingDastBuildsWithVariables, feature_category: :dynamic_application_security_testing do
- it 'is a no-op' do
- migrate!
- end
-end
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
deleted file mode 100644
index a9500b9f942..00000000000
--- a/spec/migrations/backfill_cadence_id_for_boards_scoped_to_iteration_spec.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillCadenceIdForBoardsScopedToIteration, :migration, feature_category: :team_planning do
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:iterations_cadences) { table(:iterations_cadences) }
- let(:boards) { table(:boards) }
-
- let!(:group) { namespaces.create!(name: 'group1', path: 'group1', type: 'Group') }
- let!(:cadence) { iterations_cadences.create!(title: 'group cadence', group_id: group.id, start_date: Time.current) }
- let!(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) }
- let!(:project_board1) { boards.create!(name: 'Project Dev1', project_id: project.id) }
- let!(:project_board2) { boards.create!(name: 'Project Dev2', project_id: project.id, iteration_id: -4) }
- let!(:project_board3) { boards.create!(name: 'Project Dev3', project_id: project.id, iteration_id: -4) }
- let!(:project_board4) { boards.create!(name: 'Project Dev4', project_id: project.id, iteration_id: -4) }
-
- let!(:group_board1) { boards.create!(name: 'Group Dev1', group_id: group.id) }
- let!(:group_board2) { boards.create!(name: 'Group Dev2', group_id: group.id, iteration_id: -4) }
- let!(:group_board3) { boards.create!(name: 'Group Dev3', group_id: group.id, iteration_id: -4) }
- let!(:group_board4) { boards.create!(name: 'Group Dev4', group_id: group.id, iteration_id: -4) }
-
- describe '#up' do
- it 'schedules background migrations' do
- Sidekiq::Testing.fake! do
- freeze_time do
- described_class.new.up
-
- migration = described_class::MIGRATION
-
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'group', 'up', group_board2.id, group_board4.id)
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'project', 'up', project_board2.id, project_board4.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq 2
- end
- end
- end
-
- context 'in batches' do
- before do
- stub_const('BackfillCadenceIdForBoardsScopedToIteration::BATCH_SIZE', 2)
- end
-
- it 'schedules background migrations' do
- Sidekiq::Testing.fake! do
- freeze_time do
- described_class.new.up
-
- migration = described_class::MIGRATION
-
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'group', 'up', group_board2.id, group_board3.id)
- expect(migration).to be_scheduled_delayed_migration(4.minutes, 'group', 'up', group_board4.id, group_board4.id)
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'project', 'up', project_board2.id, project_board3.id)
- expect(migration).to be_scheduled_delayed_migration(4.minutes, 'project', 'up', project_board4.id, project_board4.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq 4
- end
- end
- end
- end
- end
-
- describe '#down' do
- let!(:project_board1) { boards.create!(name: 'Project Dev1', project_id: project.id) }
- let!(:project_board2) { boards.create!(name: 'Project Dev2', project_id: project.id, iteration_cadence_id: cadence.id) }
- let!(:project_board3) { boards.create!(name: 'Project Dev3', project_id: project.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
- let!(:project_board4) { boards.create!(name: 'Project Dev4', project_id: project.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
-
- let!(:group_board1) { boards.create!(name: 'Group Dev1', group_id: group.id) }
- let!(:group_board2) { boards.create!(name: 'Group Dev2', group_id: group.id, iteration_cadence_id: cadence.id) }
- let!(:group_board3) { boards.create!(name: 'Group Dev3', group_id: group.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
- let!(:group_board4) { boards.create!(name: 'Group Dev4', group_id: group.id, iteration_id: -4, iteration_cadence_id: cadence.id) }
-
- it 'schedules background migrations' do
- Sidekiq::Testing.fake! do
- freeze_time do
- described_class.new.down
-
- migration = described_class::MIGRATION
-
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'none', 'down', project_board2.id, group_board4.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq 1
- end
- end
- end
-
- context 'in batches' do
- before do
- stub_const('BackfillCadenceIdForBoardsScopedToIteration::BATCH_SIZE', 2)
- end
-
- it 'schedules background migrations' do
- Sidekiq::Testing.fake! do
- freeze_time do
- described_class.new.down
-
- migration = described_class::MIGRATION
-
- expect(migration).to be_scheduled_delayed_migration(2.minutes, 'none', 'down', project_board2.id, project_board3.id)
- expect(migration).to be_scheduled_delayed_migration(4.minutes, 'none', 'down', project_board4.id, group_board2.id)
- expect(migration).to be_scheduled_delayed_migration(6.minutes, 'none', 'down', group_board3.id, group_board4.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq 3
- end
- end
- end
- end
- end
-end
diff --git a/spec/migrations/backfill_integrations_type_new_spec.rb b/spec/migrations/backfill_integrations_type_new_spec.rb
deleted file mode 100644
index 79519c4439a..00000000000
--- a/spec/migrations/backfill_integrations_type_new_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillIntegrationsTypeNew, feature_category: :integrations do
- let!(:migration) { described_class::MIGRATION }
- let!(:integrations) { table(:integrations) }
-
- before do
- integrations.create!(id: 1)
- integrations.create!(id: 2)
- integrations.create!(id: 3)
- integrations.create!(id: 4)
- integrations.create!(id: 5)
- end
-
- describe '#up' do
- it 'schedules background jobs for each batch of integrations' do
- migrate!
-
- expect(migration).to have_scheduled_batched_migration(
- table_name: :integrations,
- column_name: :id,
- interval: described_class::INTERVAL
- )
- 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/backfill_issues_upvotes_count_spec.rb b/spec/migrations/backfill_issues_upvotes_count_spec.rb
deleted file mode 100644
index b8687595b35..00000000000
--- a/spec/migrations/backfill_issues_upvotes_count_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillIssuesUpvotesCount, feature_category: :team_planning do
- let(:migration) { described_class.new }
- let(:issues) { table(:issues) }
- let(:award_emoji) { table(:award_emoji) }
-
- let!(:issue1) { issues.create! }
- let!(:issue2) { issues.create! }
- let!(:issue3) { issues.create! }
- let!(:issue4) { issues.create! }
- let!(:issue4_without_thumbsup) { issues.create! }
-
- let!(:award_emoji1) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue1.id) }
- let!(:award_emoji2) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue2.id) }
- let!(:award_emoji3) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue3.id) }
- let!(:award_emoji4) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue4.id) }
-
- it 'correctly schedules background migrations', :aggregate_failures do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_migration(issue1.id, issue2.id)
- expect(described_class::MIGRATION).to be_scheduled_migration(issue3.id, issue4.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
-end
diff --git a/spec/migrations/backfill_stage_event_hash_spec.rb b/spec/migrations/backfill_stage_event_hash_spec.rb
deleted file mode 100644
index 399a9c4dfde..00000000000
--- a/spec/migrations/backfill_stage_event_hash_spec.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe BackfillStageEventHash, schema: 20210730103808, feature_category: :value_stream_management do
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:labels) { table(:labels) }
- let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
- let(:project_stages) { table(:analytics_cycle_analytics_project_stages) }
- let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
- let(:project_value_streams) { table(:analytics_cycle_analytics_project_value_streams) }
- let(:stage_event_hashes) { table(:analytics_cycle_analytics_stage_event_hashes) }
-
- let(:issue_created) { 1 }
- let(:issue_closed) { 3 }
- let(:issue_label_removed) { 9 }
- let(:unknown_stage_event) { -1 }
-
- let(:namespace) { namespaces.create!(name: 'ns', path: 'ns', type: 'Group') }
- let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
- let(:group_label) { labels.create!(title: 'label', type: 'GroupLabel', group_id: namespace.id) }
- let(:group_value_stream) { group_value_streams.create!(name: 'group vs', group_id: namespace.id) }
- let(:project_value_stream) { project_value_streams.create!(name: 'project vs', project_id: project.id) }
-
- let(:group_stage_1) do
- group_stages.create!(
- name: 'stage 1',
- group_id: namespace.id,
- start_event_identifier: issue_created,
- end_event_identifier: issue_closed,
- group_value_stream_id: group_value_stream.id
- )
- end
-
- let(:group_stage_2) do
- group_stages.create!(
- name: 'stage 2',
- group_id: namespace.id,
- start_event_identifier: issue_created,
- end_event_identifier: issue_label_removed,
- end_event_label_id: group_label.id,
- group_value_stream_id: group_value_stream.id
- )
- end
-
- let(:project_stage_1) do
- project_stages.create!(
- name: 'stage 1',
- project_id: project.id,
- start_event_identifier: issue_created,
- end_event_identifier: issue_closed,
- project_value_stream_id: project_value_stream.id
- )
- end
-
- let(:invalid_group_stage) do
- group_stages.create!(
- name: 'stage 3',
- group_id: namespace.id,
- start_event_identifier: issue_created,
- end_event_identifier: unknown_stage_event,
- group_value_stream_id: group_value_stream.id
- )
- end
-
- describe '#up' do
- it 'populates stage_event_hash_id column' do
- group_stage_1
- group_stage_2
- project_stage_1
-
- migrate!
-
- group_stage_1.reload
- group_stage_2.reload
- project_stage_1.reload
-
- expect(group_stage_1.stage_event_hash_id).not_to be_nil
- expect(group_stage_2.stage_event_hash_id).not_to be_nil
- expect(project_stage_1.stage_event_hash_id).not_to be_nil
-
- expect(stage_event_hashes.count).to eq(2) # group_stage_1 and project_stage_1 has the same hash
- end
-
- it 'runs without problem without stages' do
- expect { migrate! }.not_to raise_error
- end
-
- context 'when invalid event identifier is discovered' do
- it 'removes the stage' do
- group_stage_1
- invalid_group_stage
-
- expect { migrate! }.not_to change { group_stage_1 }
-
- expect(group_stages.find_by_id(invalid_group_stage.id)).to eq(nil)
- end
- end
- end
-end
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 e8dce46bdbc..7c9d2e3170a 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, feature_category: :users do
+RSpec.describe CleanupAfterFixingIssueWhenAdminChangedPrimaryEmail, :sidekiq, feature_category: :user_profile 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 01ceef9f3a1..ce7be6aed73 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, feature_category: :users do
+RSpec.describe CleanupAfterFixingRegressionWithNewUsersEmails, :sidekiq, feature_category: :user_profile do
let(:migration) { described_class.new }
let(:users) { table(:users) }
let(:emails) { table(:emails) }
diff --git a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb b/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
deleted file mode 100644
index 598030c99a0..00000000000
--- a/spec/migrations/cleanup_remaining_orphan_invites_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe CleanupRemainingOrphanInvites, :migration, feature_category: :subgroups do
- def create_member(**extra_attributes)
- defaults = {
- access_level: 10,
- source_id: 1,
- source_type: "Project",
- notification_level: 0,
- type: 'ProjectMember'
- }
-
- table(:members).create!(defaults.merge(extra_attributes))
- end
-
- def create_user(**extra_attributes)
- defaults = { projects_limit: 0 }
- table(:users).create!(defaults.merge(extra_attributes))
- end
-
- describe '#up', :aggregate_failures do
- it 'removes invite tokens for accepted records' do
- record1 = create_member(invite_token: 'foo', user_id: nil)
- record2 = create_member(invite_token: 'foo2', user_id: create_user(username: 'foo', email: 'foo@example.com').id)
- record3 = create_member(invite_token: nil, user_id: create_user(username: 'bar', email: 'bar@example.com').id)
-
- migrate!
-
- expect(table(:members).find(record1.id).invite_token).to eq 'foo'
- expect(table(:members).find(record2.id).invite_token).to eq nil
- expect(table(:members).find(record3.id).invite_token).to eq nil
- end
- end
-end
diff --git a/spec/migrations/confirm_security_bot_spec.rb b/spec/migrations/confirm_security_bot_spec.rb
deleted file mode 100644
index 3761c113692..00000000000
--- a/spec/migrations/confirm_security_bot_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ConfirmSecurityBot, :migration, feature_category: :users do
- let(:users) { table(:users) }
-
- let(:user_type) { 8 }
-
- context 'when bot is not created' do
- it 'skips migration' do
- migrate!
-
- bot = users.find_by(user_type: user_type)
-
- expect(bot).to be_nil
- end
- end
-
- context 'when bot is confirmed' do
- let(:bot) { table(:users).create!(user_type: user_type, confirmed_at: Time.current, projects_limit: 1) }
-
- it 'skips migration' do
- expect { migrate! }.not_to change { bot.reload.confirmed_at }
- end
- end
-
- context 'when bot is not confirmed' do
- let(:bot) { table(:users).create!(user_type: user_type, projects_limit: 1) }
-
- it 'update confirmed_at' do
- freeze_time do
- expect { migrate! }.to change { bot.reload.confirmed_at }.from(nil).to(Time.current)
- end
- end
- end
-end
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
deleted file mode 100644
index 1d948257fcc..00000000000
--- a/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let(:namespaces) { table(:namespaces) }
-
- let!(:namespace) { namespaces.create!(name: 'test', path: 'test') }
- let!(:project) { projects.create!(id: 1, namespace_id: namespace.id, name: 'gitlab1') }
- let!(:container_expiration_policy) { container_expiration_policies.create!(project_id: project.id, enabled: true) }
-
- before do
- projects.create!(id: 2, namespace_id: namespace.id, name: 'gitlab2')
- container_expiration_policies.create!(project_id: 2, enabled: true)
- container_repositories.create!(id: 1, project_id: 2, name: 'image2')
-
- projects.create!(id: 3, namespace_id: namespace.id, name: 'gitlab3')
- container_expiration_policies.create!(project_id: 3, enabled: false)
- container_repositories.create!(id: 2, project_id: 3, name: 'image3')
- end
-
- it 'correctly disable expiration policies linked to no container images' do
- expect(enabled_policies.count).to eq 2
- expect(disabled_policies.count).to eq 1
- expect(container_expiration_policy.enabled).to eq true
-
- migrate!
-
- expect(enabled_policies.count).to eq 1
- expect(disabled_policies.count).to eq 2
- expect(container_expiration_policy.reload.enabled).to eq false
- end
-
- def enabled_policies
- container_expiration_policies.where(enabled: true)
- end
-
- def disabled_policies
- container_expiration_policies.where(enabled: false)
- end
-end
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
deleted file mode 100644
index 8def53e1858..00000000000
--- a/spec/migrations/fix_batched_migrations_old_format_job_arguments_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-# rubocop:disable Style/WordArray
-RSpec.describe FixBatchedMigrationsOldFormatJobArguments, feature_category: :users do
- let(:batched_background_migrations) { table(:batched_background_migrations) }
-
- context 'when migrations with legacy job arguments exists' do
- it 'updates job arguments to current format' do
- legacy_events_migration = create_batched_migration('events', 'id', ['id', 'id_convert_to_bigint'])
- legacy_push_event_payloads_migration = create_batched_migration('push_event_payloads', 'event_id', ['event_id', 'event_id_convert_to_bigint'])
-
- migrate!
-
- expect(legacy_events_migration.reload.job_arguments).to eq([['id'], ['id_convert_to_bigint']])
- expect(legacy_push_event_payloads_migration.reload.job_arguments).to eq([['event_id'], ['event_id_convert_to_bigint']])
- end
- end
-
- context 'when only migrations with current job arguments exists' do
- it 'updates nothing' do
- events_migration = create_batched_migration('events', 'id', [['id'], ['id_convert_to_bigint']])
- push_event_payloads_migration = create_batched_migration('push_event_payloads', 'event_id', [['event_id'], ['event_id_convert_to_bigint']])
-
- migrate!
-
- expect(events_migration.reload.job_arguments).to eq([['id'], ['id_convert_to_bigint']])
- expect(push_event_payloads_migration.reload.job_arguments).to eq([['event_id'], ['event_id_convert_to_bigint']])
- end
- end
-
- context 'when migrations with both legacy and current job arguments exist' do
- it 'updates nothing' do
- legacy_events_migration = create_batched_migration('events', 'id', ['id', 'id_convert_to_bigint'])
- events_migration = create_batched_migration('events', 'id', [['id'], ['id_convert_to_bigint']])
- legacy_push_event_payloads_migration = create_batched_migration('push_event_payloads', 'event_id', ['event_id', 'event_id_convert_to_bigint'])
- push_event_payloads_migration = create_batched_migration('push_event_payloads', 'event_id', [['event_id'], ['event_id_convert_to_bigint']])
-
- migrate!
-
- expect(legacy_events_migration.reload.job_arguments).to eq(['id', 'id_convert_to_bigint'])
- expect(events_migration.reload.job_arguments).to eq([['id'], ['id_convert_to_bigint']])
- expect(legacy_push_event_payloads_migration.reload.job_arguments).to eq(['event_id', 'event_id_convert_to_bigint'])
- expect(push_event_payloads_migration.reload.job_arguments).to eq([['event_id'], ['event_id_convert_to_bigint']])
- end
- end
-
- def create_batched_migration(table_name, column_name, job_arguments)
- batched_background_migrations.create!(
- max_value: 10,
- batch_size: 10,
- sub_batch_size: 10,
- interval: 1,
- job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
- table_name: table_name,
- column_name: column_name,
- job_arguments: job_arguments
- )
- end
-end
-# rubocop:enable Style/WordArray
diff --git a/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb b/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb
deleted file mode 100644
index 1385b67b607..00000000000
--- a/spec/migrations/generate_customers_dot_jwt_signing_key_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe GenerateCustomersDotJwtSigningKey, feature_category: :customersdot_application do
- let(:application_settings) do
- Class.new(ActiveRecord::Base) do
- self.table_name = 'application_settings'
-
- attr_encrypted :customers_dot_jwt_signing_key, {
- mode: :per_attribute_iv,
- key: Gitlab::Utils.ensure_utf8_size(Rails.application.secrets.db_key_base, bytes: 32.bytes),
- algorithm: 'aes-256-gcm',
- encode: true
- }
- end
- end
-
- it 'generates JWT signing key' do
- application_settings.create!
-
- reversible_migration do |migration|
- migration.before -> {
- settings = application_settings.first
-
- expect(settings.customers_dot_jwt_signing_key).to be_nil
- expect(settings.encrypted_customers_dot_jwt_signing_key).to be_nil
- expect(settings.encrypted_customers_dot_jwt_signing_key_iv).to be_nil
- }
-
- migration.after -> {
- settings = application_settings.first
-
- expect(settings.encrypted_customers_dot_jwt_signing_key).to be_present
- expect(settings.encrypted_customers_dot_jwt_signing_key_iv).to be_present
- expect { OpenSSL::PKey::RSA.new(settings.customers_dot_jwt_signing_key) }.not_to raise_error
- }
- end
- end
-end
diff --git a/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb b/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
deleted file mode 100644
index 2f62147da9d..00000000000
--- a/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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) }
- let(:builds) { table(:ci_builds) }
-
- before do
- namespaces.create!(id: 123, name: 'sample', path: 'sample')
- projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123)
-
- builds.create!(id: 1, project_id: 123, status: 'pending', protected: false, type: 'Ci::Build')
- builds.create!(id: 2, project_id: 123, status: 'pending', protected: true, type: 'Ci::Build')
- builds.create!(id: 3, project_id: 123, status: 'pending', protected: false, type: 'Ci::Build')
- builds.create!(id: 4, project_id: 123, status: 'pending', protected: true, type: 'Ci::Bridge')
- builds.create!(id: 5, project_id: 123, status: 'success', protected: true, type: 'Ci::Build')
-
- queue.create!(id: 1, project_id: 123, build_id: 1)
- queue.create!(id: 2, project_id: 123, build_id: 2)
- queue.create!(id: 3, project_id: 123, build_id: 3)
- end
-
- it 'updates entries that should be protected' do
- migrate!
-
- expect(queue.where(protected: true).count).to eq 1
- expect(queue.find_by(protected: true).id).to eq 2
- end
-end
diff --git a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb b/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
deleted file mode 100644
index 56f47fca864..00000000000
--- a/spec/migrations/orphaned_invite_tokens_cleanup_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe OrphanedInviteTokensCleanup, :migration, feature_category: :subgroups do
- def create_member(**extra_attributes)
- defaults = {
- access_level: 10,
- source_id: 1,
- source_type: "Project",
- notification_level: 0,
- type: 'ProjectMember'
- }
-
- table(:members).create!(defaults.merge(extra_attributes))
- end
-
- shared_examples 'removes orphaned invite tokens' do
- it 'removes invite tokens for accepted records with invite_accepted_at < created_at' do
- record1 = create_member(invite_token: 'foo', invite_accepted_at: 1.day.ago, created_at: 1.hour.ago)
- record2 = create_member(invite_token: 'foo2', invite_accepted_at: nil, created_at: 1.hour.ago)
- record3 = create_member(invite_token: 'foo3', invite_accepted_at: 1.day.ago, created_at: 1.year.ago)
-
- migrate!
-
- expect(table(:members).find(record1.id).invite_token).to eq nil
- expect(table(:members).find(record2.id).invite_token).to eq 'foo2'
- expect(table(:members).find(record3.id).invite_token).to eq 'foo3'
- end
- end
-
- describe '#up', :aggregate_failures do
- it_behaves_like 'removes orphaned invite tokens'
- end
-
- context 'when there is a mix of timestamptz and timestamp types' do
- around do |example|
- ActiveRecord::Base.connection.execute "ALTER TABLE members alter created_at type timestamp with time zone"
-
- example.run
-
- ActiveRecord::Base.connection.execute "ALTER TABLE members alter created_at type timestamp without time zone"
- end
-
- describe '#up', :aggregate_failures do
- it_behaves_like 'removes orphaned invite tokens'
- end
- end
-end
diff --git a/spec/migrations/queue_backfill_user_details_fields_spec.rb b/spec/migrations/queue_backfill_user_details_fields_spec.rb
index e77a66907de..4613a85be40 100644
--- a/spec/migrations/queue_backfill_user_details_fields_spec.rb
+++ b/spec/migrations/queue_backfill_user_details_fields_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueueBackfillUserDetailsFields, feature_category: :users do
+RSpec.describe QueueBackfillUserDetailsFields, feature_category: :user_profile do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
diff --git a/spec/migrations/queue_populate_projects_star_count_spec.rb b/spec/migrations/queue_populate_projects_star_count_spec.rb
index 84565d14d52..b30bb6a578b 100644
--- a/spec/migrations/queue_populate_projects_star_count_spec.rb
+++ b/spec/migrations/queue_populate_projects_star_count_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe QueuePopulateProjectsStarCount, feature_category: :users do
+RSpec.describe QueuePopulateProjectsStarCount, feature_category: :user_profile do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
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
deleted file mode 100644
index 5ebe6787f15..00000000000
--- a/spec/migrations/re_schedule_latest_pipeline_id_population_with_all_security_related_artifact_types_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ReScheduleLatestPipelineIdPopulationWithAllSecurityRelatedArtifactTypes,
- :suppress_gitlab_schemas_validate_connection, feature_category: :vulnerability_management do
- let(:namespaces) { table(:namespaces) }
- let(:pipelines) { table(:ci_pipelines) }
- let(:projects) { table(:projects) }
- let(:project_settings) { table(:project_settings) }
- let(:vulnerability_statistics) { table(:vulnerability_statistics) }
-
- let(:letter_grade_a) { 0 }
-
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project_1) { projects.create!(namespace_id: namespace.id, name: 'Foo 1') }
- let(:project_2) { projects.create!(namespace_id: namespace.id, name: 'Foo 2') }
- let(:project_3) { projects.create!(namespace_id: namespace.id, name: 'Foo 3') }
- let(:project_4) { projects.create!(namespace_id: namespace.id, name: 'Foo 4') }
-
- before do
- project_settings.create!(project_id: project_1.id, has_vulnerabilities: true)
- project_settings.create!(project_id: project_2.id, has_vulnerabilities: true)
- project_settings.create!(project_id: project_3.id)
- project_settings.create!(project_id: project_4.id, has_vulnerabilities: true)
-
- pipeline = pipelines.create!(project_id: project_2.id, ref: 'master', sha: 'adf43c3a')
-
- vulnerability_statistics.create!(project_id: project_2.id, letter_grade: letter_grade_a, latest_pipeline_id: pipeline.id)
- vulnerability_statistics.create!(project_id: project_4.id, letter_grade: letter_grade_a)
-
- allow(Gitlab).to receive(:ee?).and_return(is_ee?)
- stub_const("#{described_class.name}::BATCH_SIZE", 1)
- end
-
- around do |example|
- freeze_time { example.run }
- end
-
- context 'when the installation is FOSS' do
- let(:is_ee?) { false }
-
- it 'does not schedule any background job' do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(0)
- end
- end
-
- context 'when the installation is EE' do
- let(:is_ee?) { true }
-
- it 'schedules the background jobs' do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to be(2)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(described_class::DELAY_INTERVAL, project_1.id, project_1.id)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2 * described_class::DELAY_INTERVAL, project_4.id, project_4.id)
- end
- end
-end
diff --git a/spec/migrations/sanitize_confidential_note_todos_spec.rb b/spec/migrations/recount_epic_cache_counts_v3_spec.rb
index 142378e07e1..24b89ab30ca 100644
--- a/spec/migrations/sanitize_confidential_note_todos_spec.rb
+++ b/spec/migrations/recount_epic_cache_counts_v3_spec.rb
@@ -1,10 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-
require_migration!
-RSpec.describe SanitizeConfidentialNoteTodos, feature_category: :team_planning do
+RSpec.describe RecountEpicCacheCountsV3, :migration, feature_category: :portfolio_management do
let(:migration) { described_class::MIGRATION }
describe '#up' do
@@ -12,7 +11,7 @@ RSpec.describe SanitizeConfidentialNoteTodos, feature_category: :team_planning d
migrate!
expect(migration).to have_scheduled_batched_migration(
- table_name: :notes,
+ table_name: :epics,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
diff --git a/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb b/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb
deleted file mode 100644
index 2b21dc3b67f..00000000000
--- a/spec/migrations/remove_duplicate_dast_site_tokens_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let!(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') }
- let!(:project1) { projects.create!(id: 1, namespace_id: namespace.id, path: 'project1') }
- # create non duplicate dast site token
- let!(:dast_site_token1) { dast_site_tokens.create!(project_id: project1.id, url: 'https://gitlab.com', token: SecureRandom.uuid) }
-
- context 'when duplicate dast site tokens exists' do
- # create duplicate dast site token
- 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) }
- let!(:dast_site_token3) { dast_site_tokens.create!(project_id: project2.id, url: 'https://temp_url.com', token: SecureRandom.uuid) }
- let!(:dast_site_token4) { dast_site_tokens.create!(project_id: project2.id, url: 'https://other_temp_url.com', token: SecureRandom.uuid) }
-
- before 'update URL to bypass uniqueness validation' do
- dast_site_tokens.where(project_id: 2).update_all(url: duplicate_url)
- end
-
- describe 'migration up' do
- it 'does remove duplicated dast site tokens' do
- expect(dast_site_tokens.count).to eq(4)
- expect(dast_site_tokens.where(project_id: 2, url: duplicate_url).size).to eq(3)
-
- migrate!
-
- expect(dast_site_tokens.count).to eq(2)
- expect(dast_site_tokens.where(project_id: 2, url: duplicate_url).size).to eq(1)
- end
- end
- end
-
- context 'when duplicate dast site tokens does not exists' do
- before do
- dast_site_tokens.create!(project_id: 1, url: 'https://about.gitlab.com/handbook', token: SecureRandom.uuid)
- end
-
- describe 'migration up' do
- it 'does remove duplicated dast site tokens' do
- expect { migrate! }.not_to change(dast_site_tokens, :count)
- end
- end
- end
-end
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
deleted file mode 100644
index 6cc25b74d02..00000000000
--- a/spec/migrations/remove_duplicate_dast_site_tokens_with_same_token_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let!(:namespace) { namespaces.create!(id: 1, name: 'group', path: 'group') }
- let!(:project1) { projects.create!(id: 1, namespace_id: namespace.id, path: 'project1') }
- # create non duplicate dast site token
- let!(:dast_site_token1) { dast_site_tokens.create!(project_id: project1.id, url: 'https://gitlab.com', token: SecureRandom.uuid) }
-
- context 'when duplicate dast site tokens exists' do
- # create duplicate dast site 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) }
- let!(:dast_site_token3) { dast_site_tokens.create!(project_id: project2.id, url: 'https://gitlab3.com', token: duplicate_token) }
- let!(:dast_site_token4) { dast_site_tokens.create!(project_id: project2.id, url: 'https://gitlab4.com', token: duplicate_token) }
-
- let!(:project3) { projects.create!(id: 3, namespace_id: namespace.id, path: 'project3') }
- let!(:dast_site_token5) { dast_site_tokens.create!(project_id: project3.id, url: 'https://gitlab2.com', token: other_duplicate_token) }
- let!(:dast_site_token6) { dast_site_tokens.create!(project_id: project3.id, url: 'https://gitlab3.com', token: other_duplicate_token) }
- let!(:dast_site_token7) { dast_site_tokens.create!(project_id: project3.id, url: 'https://gitlab4.com', token: other_duplicate_token) }
-
- describe 'migration up' do
- it 'does remove duplicated dast site tokens with the same token' do
- expect(dast_site_tokens.count).to eq(7)
- expect(dast_site_tokens.where(token: duplicate_token).size).to eq(3)
-
- migrate!
-
- expect(dast_site_tokens.count).to eq(3)
- expect(dast_site_tokens.where(token: duplicate_token).size).to eq(1)
- end
- end
- end
-
- context 'when duplicate dast site tokens do not exist' do
- let!(:dast_site_token5) { dast_site_tokens.create!(project_id: 1, url: 'https://gitlab5.com', token: SecureRandom.uuid) }
-
- describe 'migration up' do
- it 'does not remove any dast site tokens' do
- expect { migrate! }.not_to change(dast_site_tokens, :count)
- end
- end
- end
-end
diff --git a/spec/migrations/remove_invalid_deploy_access_level_spec.rb b/spec/migrations/remove_invalid_deploy_access_level_spec.rb
new file mode 100644
index 00000000000..cc0f5679dda
--- /dev/null
+++ b/spec/migrations/remove_invalid_deploy_access_level_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveInvalidDeployAccessLevel, :migration, feature_category: :continuous_integration do
+ let(:users) { table(:users) }
+ let(:groups) { table(:namespaces) }
+ let(:protected_environments) { table(:protected_environments) }
+ let(:deploy_access_levels) { table(:protected_environment_deploy_access_levels) }
+
+ let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) }
+ let(:group) { groups.create!(name: 'test-group', path: 'test-group') }
+ let(:pe) do
+ protected_environments.create!(name: 'test-pe', group_id: group.id)
+ end
+
+ let!(:invalid_access_level) do
+ deploy_access_levels.create!(
+ access_level: 40,
+ user_id: user.id,
+ group_id: group.id,
+ protected_environment_id: pe.id)
+ end
+
+ let!(:group_access_level) do
+ deploy_access_levels.create!(
+ group_id: group.id,
+ protected_environment_id: pe.id)
+ end
+
+ let!(:user_access_level) do
+ deploy_access_levels.create!(
+ user_id: user.id,
+ protected_environment_id: pe.id)
+ end
+
+ it 'removes invalid access_level entries' do
+ expect { migrate! }.to change {
+ deploy_access_levels.where(
+ protected_environment_id: pe.id,
+ access_level: nil).count
+ }.from(2).to(3)
+
+ expect(invalid_access_level.reload.access_level).to be_nil
+ end
+end
diff --git a/spec/migrations/rename_services_to_integrations_spec.rb b/spec/migrations/rename_services_to_integrations_spec.rb
deleted file mode 100644
index a90b0bfabd2..00000000000
--- a/spec/migrations/rename_services_to_integrations_spec.rb
+++ /dev/null
@@ -1,255 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe RenameServicesToIntegrations, feature_category: :integrations do
- let(:migration) { described_class.new }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:integrations) { table(:integrations) }
- let(:services) { table(:services) }
-
- before do
- @namespace = namespaces.create!(name: 'foo', path: 'foo')
- @project = projects.create!(namespace_id: @namespace.id)
- end
-
- RSpec.shared_examples 'a table (or view) with triggers' do
- describe 'INSERT tracker trigger' do
- it 'sets `has_external_issue_tracker` to true when active `issue_tracker` is inserted' do
- expect do
- subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- end.to change { @project.reload.has_external_issue_tracker }.to(true)
- end
-
- it 'does not set `has_external_issue_tracker` to true when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
-
- expect do
- subject.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not set `has_external_issue_tracker` to true when inactive `issue_tracker` is inserted' do
- expect do
- subject.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not set `has_external_issue_tracker` to true when a non-`issue tracker` active integration is inserted' do
- expect do
- subject.create!(category: 'my_type', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-
- describe 'UPDATE tracker trigger' do
- it 'sets `has_external_issue_tracker` to true when `issue_tracker` is made active' do
- integration = subject.create!(category: 'issue_tracker', active: false, project_id: @project.id)
-
- expect do
- integration.update!(active: true)
- end.to change { @project.reload.has_external_issue_tracker }.to(true)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive' do
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.update!(active: false)
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is made inactive, and an inactive `issue_tracker` exists' do
- subject.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.update!(active: false)
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'does not change `has_external_issue_tracker` when `issue_tracker` is made inactive, if an active `issue_tracker` exists' do
- subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.update!(active: false)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not change `has_external_issue_tracker` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- integration = subject.create!(category: 'issue_tracker', active: false, project_id: different_project.id)
-
- expect do
- integration.update!(active: true)
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-
- describe 'DELETE tracker trigger' do
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted' do
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.delete
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'sets `has_external_issue_tracker` to false when `issue_tracker` is deleted, if an inactive `issue_tracker` still exists' do
- subject.create!(category: 'issue_tracker', active: false, project_id: @project.id)
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.delete
- end.to change { @project.reload.has_external_issue_tracker }.to(false)
- end
-
- it 'does not change `has_external_issue_tracker` when `issue_tracker` is deleted, if an active `issue_tracker` still exists' do
- subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: @project.id)
-
- expect do
- integration.delete
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
-
- it 'does not change `has_external_issue_tracker` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- integration = subject.create!(category: 'issue_tracker', active: true, project_id: different_project.id)
-
- expect do
- integration.delete
- end.not_to change { @project.reload.has_external_issue_tracker }
- end
- end
-
- describe 'INSERT wiki trigger' do
- it 'sets `has_external_wiki` to true when active `ExternalWikiService` is inserted' do
- expect do
- subject.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
- end.to change { @project.reload.has_external_wiki }.to(true)
- end
-
- it 'does not set `has_external_wiki` to true when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
-
- expect do
- subject.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when inactive `ExternalWikiService` is inserted' do
- expect do
- subject.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when active other integration is inserted' do
- expect do
- subject.create!(type: 'MyService', active: true, project_id: @project.id)
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
-
- describe 'UPDATE wiki trigger' do
- it 'sets `has_external_wiki` to true when `ExternalWikiService` is made active' do
- integration = subject.create!(type: 'ExternalWikiService', active: false, project_id: @project.id)
-
- expect do
- integration.update!(active: true)
- end.to change { @project.reload.has_external_wiki }.to(true)
- end
-
- it 'sets `has_external_wiki` to false when `ExternalWikiService` is made inactive' do
- integration = subject.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
-
- expect do
- integration.update!(active: false)
- end.to change { @project.reload.has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- integration = subject.create!(type: 'ExternalWikiService', active: false, project_id: different_project.id)
-
- expect do
- integration.update!(active: true)
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
-
- describe 'DELETE wiki trigger' do
- it 'sets `has_external_wiki` to false when `ExternalWikiService` is deleted' do
- integration = subject.create!(type: 'ExternalWikiService', active: true, project_id: @project.id)
-
- expect do
- integration.delete
- end.to change { @project.reload.has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- integration = subject.create!(type: 'ExternalWikiService', active: true, project_id: different_project.id)
-
- expect do
- integration.delete
- end.not_to change { @project.reload.has_external_wiki }
- end
- end
- end
-
- RSpec.shared_examples 'a table (or view) without triggers' do
- specify do
- number_of_triggers = ActiveRecord::Base.connection
- .execute("SELECT count(*) FROM information_schema.triggers WHERE event_object_table = '#{subject.table_name}'")
- .first['count']
-
- expect(number_of_triggers).to eq(0)
- end
- end
-
- describe '#up' do
- before do
- # LOCK TABLE statements must be in a transaction
- ActiveRecord::Base.transaction { migrate! }
- end
-
- context 'the integrations table' do
- subject { integrations }
-
- it_behaves_like 'a table (or view) with triggers'
- end
-
- context 'the services table' do
- subject { services }
-
- it_behaves_like 'a table (or view) without triggers'
- end
- end
-
- describe '#down' do
- before do
- # LOCK TABLE statements must be in a transaction
- ActiveRecord::Base.transaction do
- migration.up
- migration.down
- end
- end
-
- context 'the services table' do
- subject { services }
-
- it_behaves_like 'a table (or view) with triggers'
- end
-
- context 'the integrations table' do
- subject { integrations }
-
- it_behaves_like 'a table (or view) without triggers'
- end
- end
-end
diff --git a/spec/migrations/replace_external_wiki_triggers_spec.rb b/spec/migrations/replace_external_wiki_triggers_spec.rb
deleted file mode 100644
index c2bc5c44c77..00000000000
--- a/spec/migrations/replace_external_wiki_triggers_spec.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe ReplaceExternalWikiTriggers, feature_category: :integrations do
- let(:migration) { described_class.new }
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:integrations) { table(:integrations) }
-
- before do
- @namespace = namespaces.create!(name: 'foo', path: 'foo')
- @project = projects.create!(namespace_id: @namespace.id)
- end
-
- def create_external_wiki_integration(**attrs)
- attrs.merge!(type_info)
-
- integrations.create!(**attrs)
- end
-
- def has_external_wiki
- !!@project.reload.has_external_wiki
- end
-
- shared_examples 'external wiki triggers' do
- describe 'INSERT trigger' do
- it 'sets `has_external_wiki` to true when active external wiki integration is inserted' do
- expect do
- create_external_wiki_integration(active: true, project_id: @project.id)
- end.to change { has_external_wiki }.to(true)
- end
-
- it 'does not set `has_external_wiki` to true when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
-
- expect do
- create_external_wiki_integration(active: true, project_id: different_project.id)
- end.not_to change { has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when inactive external wiki integration is inserted' do
- expect do
- create_external_wiki_integration(active: false, project_id: @project.id)
- end.not_to change { has_external_wiki }
- end
-
- it 'does not set `has_external_wiki` to true when active other service is inserted' do
- expect do
- integrations.create!(type_new: 'Integrations::MyService', type: 'MyService', active: true, project_id: @project.id)
- end.not_to change { has_external_wiki }
- end
- end
-
- describe 'UPDATE trigger' do
- it 'sets `has_external_wiki` to true when `ExternalWikiService` is made active' do
- service = create_external_wiki_integration(active: false, project_id: @project.id)
-
- expect do
- service.update!(active: true)
- end.to change { has_external_wiki }.to(true)
- end
-
- it 'sets `has_external_wiki` to false when integration is made inactive' do
- service = create_external_wiki_integration(active: true, project_id: @project.id)
-
- expect do
- service.update!(active: false)
- end.to change { has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = create_external_wiki_integration(active: false, project_id: different_project.id)
-
- expect do
- service.update!(active: true)
- end.not_to change { has_external_wiki }
- end
- end
-
- describe 'DELETE trigger' do
- it 'sets `has_external_wiki` to false when integration is deleted' do
- service = create_external_wiki_integration(active: true, project_id: @project.id)
-
- expect do
- service.delete
- end.to change { has_external_wiki }.to(false)
- end
-
- it 'does not change `has_external_wiki` when integration is for a different project' do
- different_project = projects.create!(namespace_id: @namespace.id)
- service = create_external_wiki_integration(active: true, project_id: different_project.id)
-
- expect do
- service.delete
- end.not_to change { has_external_wiki }
- end
- end
- end
-
- describe '#up' do
- before do
- migrate!
- end
-
- context 'when integrations are created with the new STI value' do
- let(:type_info) { { type_new: 'Integrations::ExternalWiki' } }
-
- it_behaves_like 'external wiki triggers'
- end
-
- context 'when integrations are created with the old STI value' do
- let(:type_info) { { type: 'ExternalWikiService' } }
-
- it_behaves_like 'external wiki triggers'
- end
- end
-
- describe '#down' do
- before do
- migration.up
- migration.down
- end
-
- let(:type_info) { { type: 'ExternalWikiService' } }
-
- it_behaves_like 'external wiki triggers'
- end
-end
diff --git a/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb b/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb
deleted file mode 100644
index bbc4494837a..00000000000
--- a/spec/migrations/reschedule_delete_orphaned_deployments_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let(:background_migration_jobs) { table(:background_migration_jobs) }
-
- before do
- create_deployment!(environment.id, project.id)
- create_deployment!(environment.id, project.id)
- create_deployment!(environment.id, project.id)
- create_deployment!(non_existing_record_id, project.id)
- create_deployment!(non_existing_record_id, project.id)
- create_deployment!(non_existing_record_id, project.id)
- create_deployment!(non_existing_record_id, project.id)
-
- stub_const("#{described_class}::BATCH_SIZE", 1)
- end
-
- it 'steal existing background migration jobs' do
- expect(Gitlab::BackgroundMigration).to receive(:steal).with('DeleteOrphanedDeployments')
-
- migrate!
- end
-
- it 'cleans up background migration jobs tracking records' do
- old_successful_job = background_migration_jobs.create!(
- class_name: 'DeleteOrphanedDeployments',
- status: Gitlab::Database::BackgroundMigrationJob.statuses[:succeeded],
- arguments: [table(:deployments).minimum(:id), table(:deployments).minimum(:id)]
- )
-
- old_pending_job = background_migration_jobs.create!(
- class_name: 'DeleteOrphanedDeployments',
- status: Gitlab::Database::BackgroundMigrationJob.statuses[:pending],
- arguments: [table(:deployments).maximum(:id), table(:deployments).maximum(:id)]
- )
-
- migrate!
-
- expect { old_successful_job.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { old_pending_job.reload }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'schedules DeleteOrphanedDeployments background jobs' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(7)
- table(:deployments).find_each do |deployment|
- expect(described_class::MIGRATION).to be_scheduled_migration(deployment.id, deployment.id)
- end
- end
- end
- end
-
- def create_deployment!(environment_id, project_id)
- table(:deployments).create!(
- environment_id: environment_id,
- project_id: project_id,
- ref: 'master',
- tag: false,
- sha: 'x',
- status: 1,
- iid: table(:deployments).count + 1)
- end
-end
diff --git a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb b/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
deleted file mode 100644
index 9f1180b6e24..00000000000
--- a/spec/migrations/reset_job_token_scope_enabled_again_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe ResetJobTokenScopeEnabledAgain, feature_category: :continuous_integration do
- let(:settings) { table(:project_ci_cd_settings) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id) }
- let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id) }
-
- before do
- settings.create!(id: 1, project_id: project_1.id, job_token_scope_enabled: true)
- settings.create!(id: 2, project_id: project_2.id, job_token_scope_enabled: false)
- end
-
- it 'migrates job_token_scope_enabled to be always false' do
- expect { migrate! }
- .to change { settings.where(job_token_scope_enabled: false).count }
- .from(1).to(2)
- end
-end
diff --git a/spec/migrations/reset_job_token_scope_enabled_spec.rb b/spec/migrations/reset_job_token_scope_enabled_spec.rb
deleted file mode 100644
index 4ce9078246a..00000000000
--- a/spec/migrations/reset_job_token_scope_enabled_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe ResetJobTokenScopeEnabled, feature_category: :continuous_integration do
- let(:settings) { table(:project_ci_cd_settings) }
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
- let(:project_1) { projects.create!(name: 'proj-1', path: 'gitlab-org', namespace_id: namespace.id) }
- let(:project_2) { projects.create!(name: 'proj-2', path: 'gitlab-org', namespace_id: namespace.id) }
-
- before do
- settings.create!(id: 1, project_id: project_1.id, job_token_scope_enabled: true)
- settings.create!(id: 2, project_id: project_2.id, job_token_scope_enabled: false)
- end
-
- it 'migrates job_token_scope_enabled to be always false' do
- expect { migrate! }
- .to change { settings.where(job_token_scope_enabled: false).count }
- .from(1).to(2)
- end
-end
diff --git a/spec/migrations/reset_severity_levels_to_new_default_spec.rb b/spec/migrations/reset_severity_levels_to_new_default_spec.rb
deleted file mode 100644
index 83e57b852a0..00000000000
--- a/spec/migrations/reset_severity_levels_to_new_default_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-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) }
- let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
- let(:project) { projects.create!(name: 'project', path: 'project', namespace_id: namespace.id) }
- let(:approval_project_rule) { approval_project_rules.create!(name: 'rule', project_id: project.id, severity_levels: severity_levels) }
-
- context 'without having all severity levels selected' do
- let(:severity_levels) { ['high'] }
-
- it 'does not change severity_levels' do
- expect(approval_project_rule.severity_levels).to eq(severity_levels)
- expect { migrate! }.not_to change { approval_project_rule.reload.severity_levels }
- end
- end
-
- context 'with all scanners selected' do
- let(:severity_levels) { ::Enums::Vulnerability::SEVERITY_LEVELS.keys }
- let(:default_levels) { %w(unknown high critical) }
-
- it 'changes severity_levels to the default value' do
- expect(approval_project_rule.severity_levels).to eq(severity_levels)
- expect { migrate! }.to change { approval_project_rule.reload.severity_levels }.from(severity_levels).to(default_levels)
- end
- end
-end
diff --git a/spec/migrations/retry_backfill_traversal_ids_spec.rb b/spec/migrations/retry_backfill_traversal_ids_spec.rb
deleted file mode 100644
index f3658d1b8a3..00000000000
--- a/spec/migrations/retry_backfill_traversal_ids_spec.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe RetryBackfillTraversalIds, :migration, feature_category: :subgroups do
- include ReloadHelpers
-
- let!(:namespaces_table) { table(:namespaces) }
-
- context 'when BackfillNamespaceTraversalIdsRoots jobs are pending' do
- before do
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsRoots',
- arguments: [1, 4, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
- )
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsRoots',
- arguments: [5, 9, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
- )
- end
-
- it 'queues pending jobs' do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.length).to eq(1)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsRoots', [1, 4, 100]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- end
- end
-
- context 'when BackfillNamespaceTraversalIdsChildren jobs are pending' do
- before do
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsChildren',
- arguments: [1, 4, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
- )
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsRoots',
- arguments: [5, 9, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
- )
- end
-
- it 'queues pending jobs' do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.length).to eq(1)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsChildren', [1, 4, 100]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- end
- end
-
- context 'when BackfillNamespaceTraversalIdsRoots and BackfillNamespaceTraversalIdsChildren jobs are pending' do
- before do
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsRoots',
- arguments: [1, 4, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
- )
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsChildren',
- arguments: [5, 9, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
- )
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsRoots',
- arguments: [11, 14, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
- )
- table(:background_migration_jobs).create!(
- class_name: 'BackfillNamespaceTraversalIdsChildren',
- arguments: [15, 19, 100],
- status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
- )
- end
-
- it 'queues pending jobs' do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.length).to eq(2)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsRoots', [1, 4, 100]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
- expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['BackfillNamespaceTraversalIdsChildren', [5, 9, 100]])
- expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(RetryBackfillTraversalIds::DELAY_INTERVAL.from_now.to_f)
- end
- end
- end
-end
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
deleted file mode 100644
index 63678a094a7..00000000000
--- a/spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleCopyCiBuildsColumnsToSecurityScans2, feature_category: :dependency_scanning do
- it 'is a no-op' do
- migrate!
- end
-end
diff --git a/spec/migrations/schedule_security_setting_creation_spec.rb b/spec/migrations/schedule_security_setting_creation_spec.rb
deleted file mode 100644
index edabb2a2299..00000000000
--- a/spec/migrations/schedule_security_setting_creation_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe ScheduleSecuritySettingCreation, :sidekiq, feature_category: :projects do
- describe '#up' do
- let(:projects) { table(:projects) }
- let(:namespaces) { table(:namespaces) }
-
- context 'for EE version' do
- before do
- stub_const("#{described_class.name}::BATCH_SIZE", 2)
- allow(Gitlab).to receive(:ee?).and_return(true)
- end
-
- it 'schedules background migration job' do
- namespace = namespaces.create!(name: 'test', path: 'test')
- projects.create!(id: 12, namespace_id: namespace.id, name: 'red', path: 'red')
- projects.create!(id: 13, namespace_id: namespace.id, name: 'green', path: 'green')
- projects.create!(id: 14, namespace_id: namespace.id, name: 'blue', path: 'blue')
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(5.minutes, 12, 13)
-
- expect(described_class::MIGRATION)
- .to be_scheduled_delayed_migration(10.minutes, 14, 14)
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-
- context 'for FOSS version' do
- before do
- allow(Gitlab).to receive(:ee?).and_return(false)
- end
-
- it 'does not schedule any jobs' do
- namespace = namespaces.create!(name: 'test', path: 'test')
- projects.create!(id: 12, namespace_id: namespace.id, name: 'red', path: 'red')
-
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq(0)
- end
- end
- end
- end
- end
-end
diff --git a/spec/migrations/set_default_job_token_scope_true_spec.rb b/spec/migrations/set_default_job_token_scope_true_spec.rb
deleted file mode 100644
index 25f4f07e15a..00000000000
--- a/spec/migrations/set_default_job_token_scope_true_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-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) }
-
- let(:namespace) { namespaces.create!(name: 'test', path: 'path', type: 'Group') }
- let(:project) { projects.create!(namespace_id: namespace.id) }
-
- describe '#up' do
- it 'sets the job_token_scope_enabled default to true' do
- described_class.new.up
-
- settings = ci_cd_settings.create!(project_id: project.id)
-
- expect(settings.job_token_scope_enabled).to be_truthy
- end
- end
-
- describe '#down' do
- it 'sets the job_token_scope_enabled default to false' do
- described_class.new.down
-
- settings = ci_cd_settings.create!(project_id: project.id)
-
- expect(settings.job_token_scope_enabled).to be_falsey
- end
- end
-end
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
index 4303713744e..8e00fbe4b89 100644
--- 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
@@ -3,7 +3,8 @@
require 'spec_helper'
require_migration!
-RSpec.describe SetEmailConfirmationSettingBeforeRemovingSendUserConfirmationEmailColumn, feature_category: :users do
+RSpec.describe SetEmailConfirmationSettingBeforeRemovingSendUserConfirmationEmailColumn,
+ feature_category: :user_profile do
let(:migration) { described_class.new }
let(:application_settings_table) { table(:application_settings) }
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 e08aa8679a1..ef1ced530c9 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, feature_category: :users do
+RSpec.describe SetEmailConfirmationSettingFromSendUserConfirmationEmailSetting, feature_category: :user_profile do
let(:migration) { described_class.new }
let(:application_settings_table) { table(:application_settings) }
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
deleted file mode 100644
index d2cd7a6980d..00000000000
--- a/spec/migrations/steal_merge_request_diff_commit_users_migration_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe StealMergeRequestDiffCommitUsersMigration, :migration, feature_category: :source_code_management do
- let(:migration) { described_class.new }
-
- describe '#up' do
- it 'schedules a job if there are pending jobs' do
- Gitlab::Database::BackgroundMigrationJob.create!(
- class_name: 'MigrateMergeRequestDiffCommitUsers',
- arguments: [10, 20]
- )
-
- expect(migration)
- .to receive(:migrate_in)
- .with(1.hour, 'StealMigrateMergeRequestDiffCommitUsers', [10, 20])
-
- migration.up
- end
-
- it 'does not schedule any jobs when all jobs have been completed' do
- expect(migration).not_to receive(:migrate_in)
-
- migration.up
- end
- end
-end
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
deleted file mode 100644
index efc051d9a68..00000000000
--- a/spec/migrations/update_integrations_trigger_type_new_on_insert_spec.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_migration!
-
-RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsert, feature_category: :integrations do
- let(:migration) { described_class.new }
- let(:integrations) { table(:integrations) }
-
- shared_examples 'transforms known types' do
- # This matches Gitlab::Integrations::StiType at the time the original trigger
- # was added in db/migrate/20210721135638_add_triggers_to_integrations_type_new.rb
- let(:namespaced_integrations) do
- %w[
- Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
- Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Irker Jenkins Jira Mattermost
- MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker
- Prometheus Pushover Redmine Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack
-
- Github GitlabSlackApplication
- ]
- end
-
- it 'sets `type_new` to the transformed `type` class name' do
- namespaced_integrations.each do |type|
- integration = integrations.create!(type: "#{type}Service")
-
- expect(integration.reload).to have_attributes(
- type: "#{type}Service",
- type_new: "Integrations::#{type}"
- )
- end
- end
- end
-
- describe '#up' do
- before do
- migrate!
- end
-
- describe 'INSERT trigger with dynamic mapping' do
- it_behaves_like 'transforms known types'
-
- it 'transforms unknown types if it ends in "Service"' do
- integration = integrations.create!(type: 'AcmeService')
-
- expect(integration.reload).to have_attributes(
- type: 'AcmeService',
- type_new: 'Integrations::Acme'
- )
- end
-
- it 'ignores "Service" occurring elsewhere in the type' do
- integration = integrations.create!(type: 'ServiceAcmeService')
-
- expect(integration.reload).to have_attributes(
- type: 'ServiceAcmeService',
- type_new: 'Integrations::ServiceAcme'
- )
- end
-
- it 'copies unknown types if it does not end with "Service"' do
- integration = integrations.create!(type: 'Integrations::Acme')
-
- expect(integration.reload).to have_attributes(
- type: 'Integrations::Acme',
- type_new: 'Integrations::Acme'
- )
- end
- end
- end
-
- describe '#down' do
- before do
- migration.up
- migration.down
- end
-
- describe 'INSERT trigger with static mapping' do
- it_behaves_like 'transforms known types'
-
- it 'ignores types that are already namespaced' do
- integration = integrations.create!(type: 'Integrations::Asana')
-
- expect(integration.reload).to have_attributes(
- type: 'Integrations::Asana',
- type_new: nil
- )
- end
-
- it 'ignores types that are unknown' do
- integration = integrations.create!(type: 'FooBar')
-
- expect(integration.reload).to have_attributes(
- type: 'FooBar',
- type_new: nil
- )
- end
- end
- end
-end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index b07fafabbb5..7995cc36383 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -43,6 +43,41 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
it { is_expected.not_to allow_value(javascript).for(:reported_from_url) }
it { is_expected.to allow_value('http://localhost:9000').for(:reported_from_url) }
it { is_expected.to allow_value('https://gitlab.com').for(:reported_from_url) }
+
+ it { is_expected.to allow_value([]).for(:links_to_spam) }
+ it { is_expected.to allow_value(nil).for(:links_to_spam) }
+ it { is_expected.to allow_value('').for(:links_to_spam) }
+
+ it { is_expected.to allow_value(['https://gitlab.com']).for(:links_to_spam) }
+ it { is_expected.to allow_value(['http://localhost:9000']).for(:links_to_spam) }
+
+ it { is_expected.not_to allow_value(['spam']).for(:links_to_spam) }
+ it { is_expected.not_to allow_value(['http://localhost:9000', 'spam']).for(:links_to_spam) }
+
+ it { is_expected.to allow_value(['https://gitlab.com'] * 20).for(:links_to_spam) }
+ it { is_expected.not_to allow_value(['https://gitlab.com'] * 21).for(:links_to_spam) }
+
+ it {
+ is_expected.to allow_value([
+ "https://gitlab.com/#{SecureRandom.alphanumeric(493)}"
+ ]).for(:links_to_spam)
+ }
+
+ it {
+ is_expected.not_to allow_value([
+ "https://gitlab.com/#{SecureRandom.alphanumeric(494)}"
+ ]).for(:links_to_spam)
+ }
+ end
+
+ describe 'before_validation' do
+ context 'when links to spam contains empty strings' do
+ let(:report) { create(:abuse_report, links_to_spam: ['', 'https://gitlab.com']) }
+
+ it 'removes empty strings' do
+ expect(report.links_to_spam).to match_array(['https://gitlab.com'])
+ end
+ end
end
describe '#remove_user' do
diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb
index 9a5f4eee229..d3e3e40fc0c 100644
--- a/spec/models/achievements/achievement_spec.rb
+++ b/spec/models/achievements/achievement_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::Achievement, type: :model, feature_category: :users do
+RSpec.describe Achievements::Achievement, type: :model, feature_category: :user_profile do
describe 'associations' do
it { is_expected.to belong_to(:namespace).required }
diff --git a/spec/models/achievements/user_achievement_spec.rb b/spec/models/achievements/user_achievement_spec.rb
index a91cba2b5e2..9d88bfdd477 100644
--- a/spec/models/achievements/user_achievement_spec.rb
+++ b/spec/models/achievements/user_achievement_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :users do
+RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :user_profile do
describe 'associations' do
it { is_expected.to belong_to(:achievement).inverse_of(:user_achievements).required }
it { is_expected.to belong_to(:user).inverse_of(:user_achievements).required }
diff --git a/spec/models/airflow/dags_spec.rb b/spec/models/airflow/dags_spec.rb
new file mode 100644
index 00000000000..ff3c4522779
--- /dev/null
+++ b/spec/models/airflow/dags_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Airflow::Dags, feature_category: :dataops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:dag_name) }
+ it { is_expected.to validate_length_of(:dag_name).is_at_most(255) }
+ it { is_expected.to validate_length_of(:schedule).is_at_most(255) }
+ it { is_expected.to validate_length_of(:fileloc).is_at_most(255) }
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/aggregation_spec.rb b/spec/models/analytics/cycle_analytics/aggregation_spec.rb
index a51c21dc87e..e69093f454a 100644
--- a/spec/models/analytics/cycle_analytics/aggregation_spec.rb
+++ b/spec/models/analytics/cycle_analytics/aggregation_spec.rb
@@ -158,6 +158,16 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model, feature_cat
end.not_to change { described_class.count }
end
end
+
+ context 'when the aggregation was disabled for some reason' do
+ it 're-enables the aggregation' do
+ create(:cycle_analytics_aggregation, enabled: false, namespace: group)
+
+ aggregation = described_class.safe_create_for_namespace(group)
+
+ expect(aggregation).to be_enabled
+ end
+ end
end
describe '#load_batch' do
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
deleted file mode 100644
index 3c7fde17355..00000000000
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Analytics::CycleAnalytics::ProjectStage do
- describe 'associations' do
- it { is_expected.to belong_to(:project).required }
- end
-
- it 'default stages must be valid' do
- project = build(:project)
-
- Gitlab::Analytics::CycleAnalytics::DefaultStages.all.each do |params|
- stage = described_class.new(params.merge(project: project))
- expect(stage).to be_valid
- end
- end
-
- it_behaves_like 'value stream analytics stage' do
- let(:factory) { :cycle_analytics_project_stage }
- let(:parent) { build(:project) }
- let(:parent_name) { :project }
- end
-
- context 'relative positioning' do
- it_behaves_like 'a class that supports relative positioning' do
- let_it_be(:project) { create(:project) }
- let(:factory) { :cycle_analytics_project_stage }
- let(:default_params) { { project: project } }
- end
- end
-
- describe '.distinct_stages_within_hierarchy' do
- let_it_be(:top_level_group) { create(:group) }
- let_it_be(:sub_group_1) { create(:group, parent: top_level_group) }
- let_it_be(:sub_group_2) { create(:group, parent: sub_group_1) }
-
- let_it_be(:project_1) { create(:project, group: sub_group_1) }
- let_it_be(:project_2) { create(:project, group: sub_group_2) }
- let_it_be(:project_3) { create(:project, group: top_level_group) }
-
- let_it_be(:stage1) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
- let_it_be(:stage2) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
-
- let_it_be(:stage3) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
- let_it_be(:stage4) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
-
- subject(:distinct_start_and_end_event_identifiers) { described_class.distinct_stages_within_hierarchy(top_level_group).to_a.pluck(:start_event_identifier, :end_event_identifier) }
-
- it 'returns distinct stages by start and end events (using stage_event_hash_id)' do
- expect(distinct_start_and_end_event_identifiers).to match_array(
- [
- %w[issue_created issue_deployed_to_production],
- %w[merge_request_created merge_request_merged]
- ])
- end
- end
-end
diff --git a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb
deleted file mode 100644
index d84ecedc634..00000000000
--- a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do
- describe 'associations' do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:stages) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:project) }
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_length_of(:name).is_at_most(100) }
-
- it 'validates uniqueness of name' do
- project = create(:project)
- create(:cycle_analytics_project_value_stream, name: 'test', project: project)
-
- value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project)
-
- expect(value_stream).to be_invalid
- expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
- end
- end
-
- it 'is not custom' do
- expect(described_class.new).not_to be_custom
- end
-
- describe '.build_default_value_stream' do
- it 'builds the default value stream' do
- project = build(:project)
-
- value_stream = described_class.build_default_value_stream(project)
- expect(value_stream.name).to eq('default')
- end
- end
-end
diff --git a/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
index ffddaf1e1b2..43db610af5c 100644
--- a/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
+++ b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do
let(:hash_sha256) { 'does_not_matter' }
describe 'associations' do
- it { is_expected.to have_many(:cycle_analytics_project_stages) }
+ it { is_expected.to have_many(:cycle_analytics_stages) }
end
describe 'validations' do
@@ -30,14 +30,14 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do
end
describe '.cleanup_if_unused' do
- it 'removes the record' do
+ it 'removes the record if there is no stages with given stage events hash' do
described_class.cleanup_if_unused(stage_event_hash.id)
expect(described_class.find_by_id(stage_event_hash.id)).to be_nil
end
- it 'does not remove the record' do
- id = create(:cycle_analytics_project_stage).stage_event_hash_id
+ it 'does not remove the record if at least 1 group stage for the given stage events hash exists' do
+ id = create(:cycle_analytics_stage).stage_event_hash_id
described_class.cleanup_if_unused(id)
diff --git a/spec/models/analytics/cycle_analytics/stage_spec.rb b/spec/models/analytics/cycle_analytics/stage_spec.rb
new file mode 100644
index 00000000000..57748f8942e
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/stage_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream_management do
+ describe 'uniqueness validation on name' do
+ subject { build(:cycle_analytics_stage) }
+
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to([:group_id, :group_value_stream_id]) }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ it { is_expected.to belong_to(:value_stream) }
+ end
+
+ it_behaves_like 'value stream analytics namespace models' do
+ let(:factory_name) { :cycle_analytics_stage }
+ end
+
+ it_behaves_like 'value stream analytics stage' do
+ let(:factory) { :cycle_analytics_stage }
+ let(:parent) { create(:group) }
+ let(:parent_name) { :namespace }
+ end
+
+ describe '.distinct_stages_within_hierarchy' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: sub_group).reload }
+
+ before do
+ # event identifiers are the same
+ create(:cycle_analytics_stage, name: 'Stage A1', namespace: group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A2', namespace: sub_group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A3', namespace: sub_group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A4', project: project,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+
+ create(:cycle_analytics_stage,
+ name: 'Stage B1',
+ namespace: group,
+ start_event_identifier: :merge_request_last_build_started,
+ end_event_identifier: :merge_request_last_build_finished)
+
+ create(:cycle_analytics_stage, name: 'Stage C1', project: project,
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ create(:cycle_analytics_stage, name: 'Stage C2', project: project,
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ end
+
+ it 'returns distinct stages by the event identifiers' do
+ stages = described_class.distinct_stages_within_hierarchy(group).to_a
+
+ expected_event_pairs = [
+ %w[merge_request_created merge_request_merged],
+ %w[merge_request_last_build_started merge_request_last_build_finished],
+ %w[issue_created issue_deployed_to_production]
+ ].sort
+
+ current_event_pairs = stages.map do |stage|
+ [stage.start_event_identifier, stage.end_event_identifier]
+ end.sort
+
+ expect(current_event_pairs).to eq(expected_event_pairs)
+ end
+ end
+
+ describe 'events tracking' do
+ let(:category) { described_class.to_s }
+ let(:label) { described_class.table_name }
+ let(:namespace) { create(:group) }
+ let(:action) { "database_event_#{property}" }
+ let(:value_stream) { create(:cycle_analytics_value_stream) }
+ let(:feature_flag_name) { :product_intelligence_database_event_tracking }
+ let(:stage) { described_class.create!(stage_params) }
+ let(:stage_params) do
+ {
+ namespace: namespace,
+ name: 'st1',
+ start_event_identifier: :merge_request_created,
+ end_event_identifier: :merge_request_merged,
+ group_value_stream_id: value_stream.id
+ }
+ end
+
+ let(:record_tracked_attributes) do
+ {
+ "id" => stage.id,
+ "created_at" => stage.created_at,
+ "updated_at" => stage.updated_at,
+ "relative_position" => stage.relative_position,
+ "start_event_identifier" => stage.start_event_identifier,
+ "end_event_identifier" => stage.end_event_identifier,
+ "group_id" => stage.group_id,
+ "start_event_label_id" => stage.start_event_label_id,
+ "end_event_label_id" => stage.end_event_label_id,
+ "hidden" => stage.hidden,
+ "custom" => stage.custom,
+ "name" => stage.name,
+ "group_value_stream_id" => stage.group_value_stream_id
+ }
+ end
+
+ describe '#create' do
+ it_behaves_like 'Snowplow event tracking' do
+ let(:property) { 'create' }
+ let(:extra) { record_tracked_attributes }
+
+ subject(:new_group_stage) { stage }
+ end
+ end
+
+ describe '#update', :freeze_time do
+ it_behaves_like 'Snowplow event tracking' do
+ subject(:create_group_stage) { stage.update!(name: 'st 2') }
+
+ let(:extra) { record_tracked_attributes.merge('name' => 'st 2') }
+ let(:property) { 'update' }
+ end
+ end
+
+ describe '#destroy' do
+ it_behaves_like 'Snowplow event tracking' do
+ subject(:delete_stage_group) { stage.destroy! }
+
+ let(:extra) { record_tracked_attributes }
+ let(:property) { 'destroy' }
+ end
+ end
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
new file mode 100644
index 00000000000..e32fbef30ae
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_category: :value_stream_management do
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ it { is_expected.to have_many(:stages) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(100) }
+
+ it 'validates uniqueness of name' do
+ group = create(:group)
+ create(:cycle_analytics_value_stream, name: 'test', namespace: group)
+
+ value_stream = build(:cycle_analytics_value_stream, name: 'test', namespace: group)
+
+ expect(value_stream).to be_invalid
+ expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
+ end
+
+ it_behaves_like 'value stream analytics namespace models' do
+ let(:factory_name) { :cycle_analytics_value_stream }
+ end
+ end
+
+ describe 'ordering of stages' do
+ let(:group) { create(:group) }
+ let(:value_stream) do
+ create(:cycle_analytics_value_stream, namespace: group, stages: [
+ create(:cycle_analytics_stage, namespace: group, name: "stage 1", relative_position: 5),
+ create(:cycle_analytics_stage, namespace: group, name: "stage 2", relative_position: nil),
+ create(:cycle_analytics_stage, namespace: group, name: "stage 3", relative_position: 1)
+ ])
+ end
+
+ before do
+ value_stream.reload
+ end
+
+ describe 'stages attribute' do
+ it 'sorts stages by relative position' do
+ names = value_stream.stages.map(&:name)
+ expect(names).to eq(['stage 3', 'stage 1', 'stage 2'])
+ end
+ end
+ end
+
+ describe '#custom?' do
+ context 'when value stream is not persisted' do
+ subject(:value_stream) { build(:cycle_analytics_value_stream, name: value_stream_name) }
+
+ context 'when the name of the value stream is default' do
+ let(:value_stream_name) { Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME }
+
+ it { is_expected.not_to be_custom }
+ end
+
+ context 'when the name of the value stream is not default' do
+ let(:value_stream_name) { 'value_stream_1' }
+
+ it { is_expected.to be_custom }
+ end
+ end
+
+ context 'when value stream is persisted' do
+ subject(:value_stream) { create(:cycle_analytics_value_stream, name: 'value_stream_1') }
+
+ it { is_expected.to be_custom }
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 54dc280d7ac..b5f47c950b9 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Appearance do
+ using RSpec::Parameterized::TableSyntax
subject { build(:appearance) }
it { include(CacheableAttributes) }
@@ -14,8 +15,10 @@ RSpec.describe Appearance do
subject(:appearance) { described_class.new }
it { expect(appearance.title).to eq('') }
- it { expect(appearance.pwa_short_name).to eq('') }
it { expect(appearance.description).to eq('') }
+ it { expect(appearance.pwa_name).to eq('') }
+ it { expect(appearance.pwa_short_name).to eq('') }
+ it { expect(appearance.pwa_description).to eq('') }
it { expect(appearance.new_project_guidelines).to eq('') }
it { expect(appearance.profile_image_guidelines).to eq('') }
it { expect(appearance.header_message).to eq('') }
@@ -23,6 +26,7 @@ RSpec.describe Appearance do
it { expect(appearance.message_background_color).to eq('#E75E40') }
it { expect(appearance.message_font_color).to eq('#FFFFFF') }
it { expect(appearance.email_header_and_footer_enabled).to eq(false) }
+ it { expect(Appearance::ALLOWED_PWA_ICON_SCALER_WIDTHS).to match_array([192, 512]) }
end
describe '#single_appearance_row' do
@@ -81,6 +85,19 @@ RSpec.describe Appearance do
it_behaves_like 'logo paths', logo_type
end
+ shared_examples 'icon paths sized' do |width|
+ let_it_be(:appearance) { create(:appearance, :with_pwa_icon) }
+ let_it_be(:filename) { 'dk.png' }
+ let_it_be(:expected_path) { "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/#{filename}?width=#{width}" }
+
+ it 'returns icon path with size parameter' do
+ expect(appearance.pwa_icon_path_scaled(width)).to eq(expected_path)
+ end
+ end
+
+ it_behaves_like 'icon paths sized', 192
+ it_behaves_like 'icon paths sized', 512
+
describe 'validations' do
let(:triplet) { '#000' }
let(:hex) { '#AABBCC' }
@@ -96,6 +113,41 @@ RSpec.describe Appearance do
it { is_expected.not_to allow_value('000').for(:message_font_color) }
end
+ shared_examples 'validation allows' do
+ it { is_expected.to allow_value(value).for(attribute) }
+ end
+
+ shared_examples 'validation permits with message' do
+ it { is_expected.not_to allow_value(value).for(attribute).with_message(message) }
+ end
+
+ context 'valid pwa attributes' do
+ where(:attribute, :value) do
+ :pwa_name | nil
+ :pwa_name | "G" * 255
+ :pwa_short_name | nil
+ :pwa_short_name | "S" * 255
+ :pwa_description | nil
+ :pwa_description | "T" * 2048
+ end
+
+ with_them do
+ it_behaves_like 'validation allows'
+ end
+ end
+
+ context 'invalid pwa attributes' do
+ where(:attribute, :value, :message) do
+ :pwa_name | "G" * 256 | 'is too long (maximum is 255 characters)'
+ :pwa_short_name | "S" * 256 | 'is too long (maximum is 255 characters)'
+ :pwa_description | "T" * 2049 | 'is too long (maximum is 2048 characters)'
+ end
+
+ with_them do
+ it_behaves_like 'validation permits with message'
+ end
+ end
+
describe 'email_header_and_footer_enabled' do
context 'default email_header_and_footer_enabled flag value' do
it 'returns email_header_and_footer_enabled as true' do
diff --git a/spec/models/approval_spec.rb b/spec/models/approval_spec.rb
index e2c0d5faa07..3d382c1712a 100644
--- a/spec/models/approval_spec.rb
+++ b/spec/models/approval_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Approval do
+RSpec.describe Approval, feature_category: :code_review_workflow do
context 'presence validation' do
it { is_expected.to validate_presence_of(:merge_request_id) }
it { is_expected.to validate_presence_of(:user_id) }
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index b1c65c6b9ee..56796aa1fe4 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Entity, type: :model do
+RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:bulk_import).required }
it { is_expected.to belong_to(:parent) }
@@ -17,6 +17,38 @@ RSpec.describe BulkImports::Entity, type: :model do
it { is_expected.to define_enum_for(:source_type).with_values(%i[group_entity project_entity]) }
+ context 'when formatting with regexes' do
+ subject { described_class.new(group: Group.new) }
+
+ it { is_expected.to allow_values('namespace', 'parent/namespace', 'parent/group/subgroup', '').for(:destination_namespace) }
+ it { is_expected.not_to allow_values('parent/namespace/', '/namespace', 'parent group/subgroup', '@namespace').for(:destination_namespace) }
+
+ it { is_expected.to allow_values('source', 'source/path', 'source/full/path').for(:source_full_path) }
+ it { is_expected.not_to allow_values('/source', 'http://source/path', 'sou rce/full/path', '').for(:source_full_path) }
+
+ it { is_expected.to allow_values('destination', 'destination-slug', 'new-destination-slug').for(:destination_slug) }
+
+ # it { is_expected.not_to allow_values('destination/slug', '/destination-slug', 'destination slug').for(:destination_slug) } <-- this test should
+ # succeed but it's failing possibly due to rspec caching. To ensure this case is covered see the more cumbersome test below:
+ context 'when destination_slug is invalid' do
+ let(:invalid_slugs) { ['destination/slug', '/destination-slug', 'destination slug'] }
+ let(:error_message) do
+ 'cannot start with a non-alphanumeric character except for periods or underscores, ' \
+ 'can contain only alphanumeric characters, periods, and underscores, ' \
+ 'cannot end with a period or forward slash, and has no ' \
+ 'leading or trailing forward slashes'
+ end
+
+ it 'raises an error' do
+ invalid_slugs.each do |slug|
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: slug)
+ expect(entity).not_to be_valid
+ expect(entity.errors.errors[0].message).to include(error_message)
+ end
+ end
+ end
+ end
+
context 'when associated with a group and project' do
it 'is invalid' do
entity = build(:bulk_import_entity, group: build(:group), project: build(:project))
@@ -45,6 +77,21 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity).to be_valid
end
+ it 'is invalid when destination_namespace is nil' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_namespace: nil)
+ expect(entity).not_to be_valid
+ end
+
+ it 'is invalid when destination_slug is empty' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: '')
+ expect(entity).not_to be_valid
+ end
+
+ it 'is invalid when destination_slug is nil' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: nil)
+ expect(entity).not_to be_valid
+ end
+
it 'is invalid as a project_entity' do
stub_feature_flags(bulk_import_projects: true)
@@ -345,4 +392,24 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity.full_path).to eq(nil)
end
end
+
+ describe '#default_visibility_level' do
+ context 'when entity is a group' do
+ it 'returns default group visibility' do
+ stub_application_setting(default_group_visibility: Gitlab::VisibilityLevel::PUBLIC)
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group))
+
+ expect(entity.default_visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context 'when entity is a project' do
+ it 'returns default project visibility' do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ entity = build(:bulk_import_entity, :project_entity, group: build(:group))
+
+ expect(entity.default_visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 70e977e37ba..7b307de87c7 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -37,8 +37,18 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
describe '#retryable?' do
let(:bridge) { create(:ci_bridge, :success) }
- it 'returns false' do
- expect(bridge.retryable?).to eq(false)
+ it 'returns true' do
+ expect(bridge.retryable?).to eq(true)
+ end
+
+ context 'without ci_recreate_downstream_pipeline ff' do
+ before do
+ stub_feature_flags(ci_recreate_downstream_pipeline: false)
+ end
+
+ it 'returns false' do
+ expect(bridge.retryable?).to eq(false)
+ end
end
end
@@ -570,4 +580,30 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
end
end
end
+
+ describe 'metadata partitioning', :ci_partitioning do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
+
+ let(:bridge) do
+ build(:ci_bridge, pipeline: pipeline)
+ end
+
+ it 'creates the metadata record and assigns its partition' do
+ # the factory doesn't use any metadatable setters by default
+ # so the record will be initialized by the before_validation callback
+ expect(bridge.metadata).to be_nil
+
+ expect(bridge.save!).to be_truthy
+
+ expect(bridge.metadata).to be_present
+ expect(bridge.metadata).to be_valid
+ expect(bridge.metadata.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+
+ describe '#deployment_job?' do
+ subject { bridge.deployment_job? }
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 8bf3af44be6..fb50ba89cd3 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -20,6 +20,10 @@ RSpec.describe Ci::BuildMetadata do
it_behaves_like 'having unique enum values'
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:runner_machine) }
+
describe '#update_timeout_state' do
subject { metadata }
diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb
index 756180621ec..bff0b35f878 100644
--- a/spec/models/ci/build_pending_state_spec.rb
+++ b/spec/models/ci/build_pending_state_spec.rb
@@ -2,7 +2,14 @@
require 'spec_helper'
-RSpec.describe Ci::BuildPendingState do
+RSpec.describe Ci::BuildPendingState, feature_category: :continuous_integration do
+ describe 'validations' do
+ subject(:pending_state) { build(:ci_build_pending_state) }
+
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to validate_presence_of(:build) }
+ end
+
describe '#crc32' do
context 'when checksum does not exist' do
let(:pending_state) do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index dd1fbd7d0d5..2b3dc97e06d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe Ci::Build, feature_category: :continuous_integration do
+RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_default: :keep do
include Ci::TemplateHelpers
include AfterNextHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:group, reload: true) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
+ let_it_be(:group, reload: true) { create_default(:group) }
+ let_it_be(:project, reload: true) { create_default(:project, :repository, group: group) }
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
+ create_default(:ci_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success')
@@ -23,17 +23,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
- it { is_expected.to have_many(:needs) }
- it { is_expected.to have_many(:sourced_pipelines) }
- it { is_expected.to have_one(:sourced_pipeline) }
- it { is_expected.to have_many(:job_variables) }
- it { is_expected.to have_many(:report_results) }
- it { is_expected.to have_many(:pages_deployments) }
+ it { is_expected.to have_many(:needs).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:sourced_pipelines).with_foreign_key(:source_job_id) }
+ it { is_expected.to have_one(:sourced_pipeline).with_foreign_key(:source_job_id) }
+ it { is_expected.to have_many(:job_variables).with_foreign_key(:job_id) }
+ it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) }
it { is_expected.to have_one(:deployment) }
- it { is_expected.to have_one(:runner_session) }
- it { is_expected.to have_one(:trace_metadata) }
- it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build) }
+ it { is_expected.to have_one(:runner_machine).through(:metadata) }
+ it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:runtime_metadata).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build).with_foreign_key(:ci_build_id) }
it { is_expected.to validate_presence_of(:ref) }
@@ -66,7 +69,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'executes hooks' do
expect_next(described_class).to receive(:execute_hooks)
- create(:ci_build)
+ create(:ci_build, pipeline: pipeline)
end
end
end
@@ -105,19 +108,19 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.ref_protected }
context 'when protected is true' do
- let!(:job) { create(:ci_build, :protected) }
+ let!(:job) { create(:ci_build, :protected, pipeline: pipeline) }
it { is_expected.to include(job) }
end
context 'when protected is false' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it { is_expected.not_to include(job) }
end
context 'when protected is nil' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
before do
job.update_attribute(:protected, nil)
@@ -131,7 +134,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_downloadable_artifacts }
context 'when job does not have a downloadable artifact' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -141,7 +144,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
::Ci::JobArtifact::DOWNLOADABLE_TYPES.each do |type|
context "when job has a #{type} artifact" do
it 'returns the job' do
- job = create(:ci_build)
+ job = create(:ci_build, pipeline: pipeline)
create(
:ci_job_artifact,
file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
@@ -155,7 +158,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when job has a non-downloadable artifact' do
- let!(:job) { create(:ci_build, :trace_artifact) }
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -167,7 +170,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_erasable_artifacts }
context 'when job does not have any artifacts' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -177,7 +180,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
::Ci::JobArtifact.erasable_file_types.each do |type|
context "when job has a #{type} artifact" do
it 'returns the job' do
- job = create(:ci_build)
+ job = create(:ci_build, pipeline: pipeline)
create(
:ci_job_artifact,
file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
@@ -191,7 +194,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when job has a non-erasable artifact' do
- let!(:job) { create(:ci_build, :trace_artifact) }
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -199,11 +202,39 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
+ describe '.with_any_artifacts' do
+ subject { described_class.with_any_artifacts }
+
+ context 'when job does not have any artifacts' do
+ it 'does not return the job' do
+ job = create(:ci_build, project: project)
+
+ is_expected.not_to include(job)
+ end
+ end
+
+ ::Ci::JobArtifact.file_types.each_key do |type|
+ context "when job has a #{type} artifact" do
+ it 'returns the job' do
+ job = create(:ci_build, project: project)
+ create(
+ :ci_job_artifact,
+ file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
+ file_type: type,
+ job: job
+ )
+
+ is_expected.to include(job)
+ end
+ end
+ end
+ end
+
describe '.with_live_trace' do
subject { described_class.with_live_trace }
context 'when build has live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:build) { create(:ci_build, :success, :trace_live, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -211,7 +242,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build does not have live trace' do
- let!(:build) { create(:ci_build, :success, :trace_artifact) }
+ let!(:build) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
it 'does not select the build' do
is_expected.to be_empty
@@ -223,7 +254,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_stale_live_trace }
context 'when build has a stale live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -231,7 +262,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build does not have a stale live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago, pipeline: pipeline) }
it 'does not select the build' do
is_expected.to be_empty
@@ -242,9 +273,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '.license_management_jobs' do
subject { described_class.license_management_jobs }
- let!(:management_build) { create(:ci_build, :success, name: :license_management) }
- let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning) }
- let!(:another_build) { create(:ci_build, :success, name: :another_type) }
+ let!(:management_build) { create(:ci_build, :success, name: :license_management, pipeline: pipeline) }
+ let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning, pipeline: pipeline) }
+ let!(:another_build) { create(:ci_build, :success, name: :another_type, pipeline: pipeline) }
it 'returns license_scanning jobs' do
is_expected.to include(scanning_build)
@@ -265,7 +296,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:date) { 1.hour.ago }
context 'when build has finished one day ago' do
- let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago) }
+ let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -273,7 +304,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build has finished 30 minutes ago' do
- let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago) }
+ let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago, pipeline: pipeline) }
it 'returns an empty array' do
is_expected.to be_empty
@@ -281,7 +312,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is still running' do
- let!(:build) { create(:ci_build, :running) }
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'returns an empty array' do
is_expected.to be_empty
@@ -292,9 +323,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '.with_exposed_artifacts' do
subject { described_class.with_exposed_artifacts }
- let!(:job1) { create(:ci_build) }
- let!(:job2) { create(:ci_build, options: options) }
- let!(:job3) { create(:ci_build) }
+ let!(:job1) { create(:ci_build, pipeline: pipeline) }
+ let!(:job2) { create(:ci_build, options: options, pipeline: pipeline) }
+ let!(:job3) { create(:ci_build, pipeline: pipeline) }
context 'when some jobs have exposed artifacs and some not' do
let(:options) { { artifacts: { expose_as: 'test', paths: ['test'] } } }
@@ -334,7 +365,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when there are multiple builds containing artifacts' do
before do
- create_list(:ci_build, 5, :success, :test_reports)
+ create_list(:ci_build, 5, :success, :test_reports, pipeline: pipeline)
end
it 'does not execute a query for selecting job artifact one by one' do
@@ -350,8 +381,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.with_needs' do
- let!(:build) { create(:ci_build) }
- let!(:build_b) { create(:ci_build) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:build_b) { create(:ci_build, pipeline: pipeline) }
let!(:build_need_a) { create(:ci_build_need, build: build) }
let!(:build_need_b) { create(:ci_build_need, build: build_b) }
@@ -390,7 +421,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#stick_build_if_status_changed' do
it 'sticks the build if the status changed' do
- job = create(:ci_build, :pending)
+ job = create(:ci_build, :pending, pipeline: pipeline)
expect(described_class.sticking).to receive(:stick)
.with(:build, job.id)
@@ -400,7 +431,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#enqueue' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites)
@@ -477,7 +508,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#enqueue_preparing' do
- let(:build) { create(:ci_build, :preparing) }
+ let(:build) { create(:ci_build, :preparing, pipeline: pipeline) }
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_unmet_prerequisites)
@@ -532,7 +563,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#run' do
context 'when build has been just created' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it 'creates queuing entry and then removes it' do
build.enqueue!
@@ -544,7 +575,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build status transition fails' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
before do
create(:ci_pending_build, build: build, project: build.project)
@@ -560,7 +591,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build has been picked by a shared runner' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
it 'creates runtime metadata entry' do
build.runner = create(:ci_runner, :instance_type)
@@ -574,7 +605,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#drop' do
context 'when has a runtime tracking entry' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
it 'removes runtime tracking entry' do
build.runner = create(:ci_runner, :instance_type)
@@ -611,10 +642,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#outdated_deployment?' do
subject { build.outdated_deployment? }
- let(:build) { create(:ci_build, :created, :with_deployment, project: project, environment: 'production') }
+ let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') }
context 'when build has no environment' do
- let(:build) { create(:ci_build, :created, project: project, environment: nil) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) }
it { expect(subject).to be_falsey }
end
@@ -644,7 +675,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is older than the latest deployment but succeeded once' do
- let(:build) { create(:ci_build, :success, :with_deployment, project: project, environment: 'production') }
+ let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
before do
allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
@@ -660,13 +691,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.schedulable? }
context 'when build is schedulable' do
- let(:build) { create(:ci_build, :created, :schedulable, project: project) }
+ let(:build) { create(:ci_build, :created, :schedulable, pipeline: pipeline) }
it { expect(subject).to be_truthy }
end
context 'when build is not schedulable' do
- let(:build) { create(:ci_build, :created, project: project) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it { expect(subject).to be_falsy }
end
@@ -679,7 +710,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
project.add_developer(user)
end
- let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
+ let(:build) { create(:ci_build, :created, :schedulable, user: user, pipeline: pipeline) }
it 'transits to scheduled' do
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
@@ -740,7 +771,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#options_scheduled_at' do
subject { build.options_scheduled_at }
- let(:build) { build_stubbed(:ci_build, options: option) }
+ let(:build) { build_stubbed(:ci_build, options: option, pipeline: pipeline) }
context 'when start_in is 1 day' do
let(:option) { { start_in: '1 day' } }
@@ -878,18 +909,18 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when new artifacts are used' do
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'is expired' do
- let(:build) { create(:ci_build, :artifacts, :expired) }
+ let(:build) { create(:ci_build, :artifacts, :expired, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -901,36 +932,32 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject(:locked_artifacts) { build.locked_artifacts? }
context 'when pipeline is artifacts_locked' do
- before do
- build.pipeline.artifacts_locked!
- end
+ let(:pipeline) { create(:ci_pipeline, locked: :artifacts_locked) }
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline is unlocked' do
- before do
- build.pipeline.unlocked!
- end
+ let(:pipeline) { create(:ci_pipeline, locked: :unlocked) }
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -938,7 +965,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#available_artifacts?' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.available_artifacts? }
@@ -997,7 +1024,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.browsable_artifacts? }
context 'artifacts metadata does exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1007,13 +1034,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.artifacts_public? }
context 'artifacts with defaults' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
context 'non public artifacts' do
- let(:build) { create(:ci_build, :artifacts, :non_public_artifacts) }
+ let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1047,7 +1074,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'artifacts archive is a zip file and metadata exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1274,12 +1301,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_live_trace?' do
subject { build.has_live_trace? }
- let(:build) { create(:ci_build, :trace_live) }
+ let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'when build does not have live trace' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -1288,12 +1315,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_archived_trace?' do
subject { build.has_archived_trace? }
- let(:build) { create(:ci_build, :trace_artifact) }
+ let(:build) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'when build does not have archived trace' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -1303,7 +1330,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.has_job_artifacts? }
context 'when build has a job artifact' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1313,13 +1340,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.has_test_reports? }
context 'when build has a test report' do
- let(:build) { create(:ci_build, :test_reports) }
+ let(:build) { create(:ci_build, :test_reports, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
context 'when build does not have a test report' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1392,7 +1419,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
with_them do
- let(:build) { create(:ci_build, trait, project: project, pipeline: pipeline) }
+ let(:build) { create(:ci_build, trait, pipeline: pipeline) }
let(:event) { state }
context "when transitioning to #{params[:state]}" do
@@ -1416,7 +1443,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'state transition as a deployable' do
subject { build.send(event) }
- let!(:build) { create(:ci_build, :with_deployment, :start_review_app, project: project, pipeline: pipeline) }
+ let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) }
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
@@ -1565,7 +1592,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'transitions to running and calls webhook' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
+ .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s }))
subject
end
@@ -1580,7 +1607,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.on_stop }
context 'when a job has a specification that it can be stopped from the other job' do
- let(:build) { create(:ci_build, :start_review_app) }
+ let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) }
it 'returns the other job name' do
is_expected.to eq('stop_review_app')
@@ -1588,7 +1615,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when a job does not have environment information' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it 'returns nil' do
is_expected.to be_nil
@@ -1663,7 +1690,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:build) do
create(:ci_build,
ref: 'master',
- environment: 'review/$CI_COMMIT_REF_NAME')
+ environment: 'review/$CI_COMMIT_REF_NAME',
+ pipeline: pipeline)
end
it { is_expected.to eq('review/master') }
@@ -1673,7 +1701,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:build) do
create(:ci_build,
yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST')
+ environment: 'review/$APP_HOST',
+ pipeline: pipeline)
end
it 'returns an expanded environment name with a list of variables' do
@@ -1695,7 +1724,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when using persisted variables' do
let(:build) do
- create(:ci_build, environment: 'review/x$CI_BUILD_ID')
+ create(:ci_build, environment: 'review/x$CI_BUILD_ID', pipeline: pipeline)
end
it { is_expected.to eq('review/x') }
@@ -1712,7 +1741,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
create(:ci_build,
ref: 'master',
yaml_variables: yaml_variables,
- environment: 'review/$ENVIRONMENT_NAME')
+ environment: 'review/$ENVIRONMENT_NAME',
+ pipeline: pipeline)
end
it { is_expected.to eq('review/master') }
@@ -1720,7 +1750,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#expanded_kubernetes_namespace' do
- let(:build) { create(:ci_build, environment: environment, options: options) }
+ let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) }
subject { build.expanded_kubernetes_namespace }
@@ -1856,7 +1886,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'build is not erasable' do
- let!(:build) { create(:ci_build) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe '#erasable?' do
subject { build.erasable? }
@@ -1867,7 +1897,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'build is erasable' do
context 'new artifacts' do
- let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts, pipeline: pipeline) }
describe '#erasable?' do
subject { build.erasable? }
@@ -1876,7 +1906,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#erased?' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts, pipeline: pipeline) }
subject { build.erased? }
@@ -1970,13 +2000,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is created' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it { is_expected.to be_cancelable }
end
context 'when build is waiting for resource' do
- let(:build) { create(:ci_build, :waiting_for_resource) }
+ let(:build) { create(:ci_build, :waiting_for_resource, pipeline: pipeline) }
it { is_expected.to be_cancelable }
end
@@ -2028,8 +2058,18 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
+ describe '#runner_machine' do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:build) { create(:ci_build, runner_machine: runner_machine) }
+
+ subject(:build_runner_machine) { described_class.find(build.id).runner_machine }
+
+ it { is_expected.to eq(runner_machine) }
+ end
+
describe '#tag_list' do
- let_it_be(:build) { create(:ci_build, tag_list: ['tag']) }
+ let_it_be(:build) { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
context 'when tags are preloaded' do
it 'does not trigger queries' do
@@ -2046,7 +2086,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#save_tags' do
- let(:build) { create(:ci_build, tag_list: ['tag']) }
+ let(:build) { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
it 'saves tags' do
build.save!
@@ -2075,13 +2115,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_tags?' do
context 'when build has tags' do
- subject { create(:ci_build, tag_list: ['tag']) }
+ subject { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
- subject { create(:ci_build, tag_list: []) }
+ subject { create(:ci_build, tag_list: [], pipeline: pipeline) }
it { is_expected.not_to have_tags }
end
@@ -2136,9 +2176,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.keep_artifacts!' do
- let!(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days) }
+ let!(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline) }
let!(:builds_for_update) do
- Ci::Build.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days).map(&:id))
+ Ci::Build.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline).map(&:id))
end
it 'resets expire_at' do
@@ -2180,7 +2220,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#keep_artifacts!' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days) }
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline) }
subject { build.keep_artifacts! }
@@ -2202,7 +2242,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#auto_retry_expected?' do
- subject { create(:ci_build, :failed) }
+ subject { create(:ci_build, :failed, pipeline: pipeline) }
context 'when build is failed and auto retry is configured' do
before do
@@ -2223,20 +2263,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
- describe '#artifacts_file_for_type' do
- let(:build) { create(:ci_build, :artifacts) }
+ describe '#artifact_for_type' do
+ let(:build) { create(:ci_build) }
+ let!(:archive) { create(:ci_job_artifact, :archive, job: build) }
+ let!(:codequality) { create(:ci_job_artifact, :codequality, job: build) }
let(:file_type) { :archive }
- subject { build.artifacts_file_for_type(file_type) }
-
- it 'queries artifacts for type' do
- expect(build).to receive_message_chain(:job_artifacts, :find_by).with(file_type: [Ci::JobArtifact.file_types[file_type]])
+ subject { build.artifact_for_type(file_type) }
- subject
- end
+ it { is_expected.to eq(archive) }
end
describe '#merge_request' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
subject { pipeline.builds.take.merge_request }
context 'on a branch pipeline' do
@@ -2281,19 +2321,23 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'on a detached merged request pipeline' do
- let(:pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, :with_job) }
+ let(:pipeline) do
+ create(:ci_pipeline, :detached_merge_request_pipeline, :with_job, merge_request: merge_request)
+ end
it { is_expected.to eq(pipeline.merge_request) }
end
context 'on a legacy detached merged request pipeline' do
- let(:pipeline) { create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job) }
+ let(:pipeline) do
+ create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job, merge_request: merge_request)
+ end
it { is_expected.to eq(pipeline.merge_request) }
end
context 'on a pipeline for merged results' do
- let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_job) }
+ let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_job, merge_request: merge_request) }
it { is_expected.to eq(pipeline.merge_request) }
end
@@ -2329,7 +2373,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when options include artifacts:expose_as' do
- let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }) }
+ let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }, pipeline: pipeline) }
it 'saves the presence of expose_as into build metadata' do
expect(build.metadata).to have_exposed_artifacts
@@ -2455,56 +2499,56 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#playable?' do
context 'when build is a manual action' do
context 'when build has been skipped' do
- subject { build_stubbed(:ci_build, :manual, status: :skipped) }
+ subject { build_stubbed(:ci_build, :manual, status: :skipped, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
context 'when build has been canceled' do
- subject { build_stubbed(:ci_build, :manual, status: :canceled) }
+ subject { build_stubbed(:ci_build, :manual, status: :canceled, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is successful' do
- subject { build_stubbed(:ci_build, :manual, status: :success) }
+ subject { build_stubbed(:ci_build, :manual, status: :success, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build has failed' do
- subject { build_stubbed(:ci_build, :manual, status: :failed) }
+ subject { build_stubbed(:ci_build, :manual, status: :failed, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is a manual untriggered action' do
- subject { build_stubbed(:ci_build, :manual, status: :manual) }
+ subject { build_stubbed(:ci_build, :manual, status: :manual, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is a manual and degenerated' do
- subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
+ subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
end
context 'when build is scheduled' do
- subject { build_stubbed(:ci_build, :scheduled) }
+ subject { build_stubbed(:ci_build, :scheduled, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is not a manual action' do
- subject { build_stubbed(:ci_build, :success) }
+ subject { build_stubbed(:ci_build, :success, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
context 'when build is waiting for deployment approval' do
- subject { build_stubbed(:ci_build, :manual, environment: 'production') }
+ subject { build_stubbed(:ci_build, :manual, environment: 'production', pipeline: pipeline) }
before do
create(:deployment, :blocked, deployable: subject)
@@ -2601,7 +2645,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it { is_expected.to be_truthy }
- context "and there are specific runner" do
+ context "and there is a project runner" do
let!(:runner) { create(:ci_runner, :project, projects: [build.project], contacted_at: 1.second.ago) }
it { is_expected.to be_falsey }
@@ -2855,7 +2899,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
before do
allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ pipeline_variables_builder = double(
+ ::Gitlab::Ci::Variables::Builder::Pipeline,
+ predefined_variables: [pipeline_pre_var]
+ )
+
allow(builder).to receive(:predefined_variables) { [build_pre_var] }
+ allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder }
end
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
@@ -2868,9 +2918,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
.to receive(:predefined_variables) { [project_pre_var] }
project.variables.create!(key: 'secret', value: 'value')
-
- allow(build.pipeline)
- .to receive(:predefined_variables).and_return([pipeline_pre_var])
end
it 'returns variables in order depending on resource hierarchy' do
@@ -3754,7 +3801,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#any_unmet_prerequisites?' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
subject { build.any_unmet_prerequisites? }
@@ -3841,7 +3888,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: any => [:preparing]' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
before do
allow(build).to receive(:prerequisites).and_return([double])
@@ -3855,7 +3902,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'when the build is waiting for deployment approval' do
- let(:build) { create(:ci_build, :manual, environment: 'production') }
+ let(:build) { create(:ci_build, :manual, environment: 'production', pipeline: pipeline) }
before do
create(:deployment, :blocked, deployable: build)
@@ -3867,7 +3914,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: any => [:pending]' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it 'queues BuildQueueWorker' do
expect(BuildQueueWorker).to receive(:perform_async).with(build.id)
@@ -3887,8 +3934,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: pending: :running' do
- let(:runner) { create(:ci_runner) }
- let(:job) { create(:ci_build, :pending, runner: runner) }
+ let_it_be_with_reload(:runner) { create(:ci_runner) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:job) { create(:ci_build, :pending, runner: runner, pipeline: pipeline) }
before do
job.project.update_attribute(:build_timeout, 1800)
@@ -3992,7 +4041,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
it { expect(job).not_to have_valid_build_dependencies }
end
@@ -4049,7 +4100,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { script: ["ls -al"], retry: 3 }, project: project, user: user) }
+ subject { create(:ci_build, :running, options: { script: ["ls -al"], retry: 3 }, pipeline: pipeline, user: user) }
it 'retries build and assigns the same user to it' do
expect_next_instance_of(::Ci::RetryJobService) do |service|
@@ -4098,7 +4149,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is not configured to be retried' do
- subject { create(:ci_build, :running, project: project, user: user, pipeline: pipeline) }
+ subject { create(:ci_build, :running, pipeline: pipeline, user: user) }
let(:pipeline) do
create(:ci_pipeline,
@@ -4162,7 +4213,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.matches_tag_ids' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
let(:tag_ids) { ::ActsAsTaggableOn::Tag.named_any(tag_list).ids }
@@ -4210,7 +4261,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.matches_tags' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
subject { described_class.where(id: build).with_any_tags }
@@ -4236,7 +4287,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'pages deployments' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
context 'when job is "pages"' do
before do
@@ -4562,7 +4613,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#artifacts_metadata_entry' do
- let_it_be(:build) { create(:ci_build, project: project) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
@@ -4622,7 +4673,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#publishes_artifacts_reports?' do
- let(:build) { create(:ci_build, options: options) }
+ let(:build) { create(:ci_build, options: options, pipeline: pipeline) }
subject { build.publishes_artifacts_reports? }
@@ -4650,7 +4701,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#runner_required_feature_names' do
- let(:build) { create(:ci_build, options: options) }
+ let(:build) { create(:ci_build, options: options, pipeline: pipeline) }
subject { build.runner_required_feature_names }
@@ -4672,7 +4723,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#supported_runner?' do
- let_it_be_with_refind(:build) { create(:ci_build) }
+ let_it_be_with_refind(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.supported_runner?(runner_features) }
@@ -4780,7 +4831,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is a last deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4788,7 +4839,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when there is a newer build with deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
@@ -4797,7 +4848,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build with deployment has failed' do
- let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4805,7 +4856,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build with deployment is running' do
- let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4815,13 +4866,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#degenerated?' do
context 'when build is degenerated' do
- subject { create(:ci_build, :degenerated) }
+ subject { create(:ci_build, :degenerated, pipeline: pipeline) }
it { is_expected.to be_degenerated }
end
context 'when build is valid' do
- subject { create(:ci_build) }
+ subject { create(:ci_build, pipeline: pipeline) }
it { is_expected.not_to be_degenerated }
@@ -4836,7 +4887,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'degenerate!' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.degenerate! }
@@ -4856,13 +4907,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#archived?' do
context 'when build is degenerated' do
- subject { create(:ci_build, :degenerated) }
+ subject { create(:ci_build, :degenerated, pipeline: pipeline) }
it { is_expected.to be_archived }
end
context 'for old build' do
- subject { create(:ci_build, created_at: 1.day.ago) }
+ subject { create(:ci_build, created_at: 1.day.ago, pipeline: pipeline) }
context 'when archive_builds_in is set' do
before do
@@ -4883,7 +4934,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#read_metadata_attribute' do
- let(:build) { create(:ci_build, :degenerated) }
+ let(:build) { create(:ci_build, :degenerated, pipeline: pipeline) }
let(:build_options) { { key: "build" } }
let(:metadata_options) { { key: "metadata" } }
let(:default_options) { { key: "default" } }
@@ -4920,7 +4971,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#write_metadata_attribute' do
- let(:build) { create(:ci_build, :degenerated) }
+ let(:build) { create(:ci_build, :degenerated, pipeline: pipeline) }
let(:options) { { key: "new options" } }
let(:existing_options) { { key: "existing options" } }
@@ -5046,13 +5097,15 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.environment_auto_stop_in }
context 'when build option has environment auto_stop_in' do
- let(:build) { create(:ci_build, options: { environment: { name: 'test', auto_stop_in: '1 day' } }) }
+ let(:build) do
+ create(:ci_build, options: { environment: { name: 'test', auto_stop_in: '1 day' } }, pipeline: pipeline)
+ end
it { is_expected.to eq('1 day') }
end
context 'when build option does not have environment auto_stop_in' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_nil }
end
@@ -5372,7 +5425,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.build_matchers' do
- let_it_be(:pipeline) { create(:ci_pipeline, :protected) }
+ let_it_be(:pipeline) { create(:ci_pipeline, :protected, project: project) }
subject(:matchers) { pipeline.builds.build_matchers(pipeline.project) }
@@ -5421,7 +5474,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#build_matcher' do
let_it_be(:build) do
- build_stubbed(:ci_build, tag_list: %w[tag1 tag2])
+ build_stubbed(:ci_build, tag_list: %w[tag1 tag2], pipeline: pipeline)
end
subject(:matcher) { build.build_matcher }
@@ -5557,7 +5610,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'does not generate cross DB queries when a record is created via FactoryBot' do
with_cross_database_modification_prevented do
- create(:ci_build)
+ create(:ci_build, pipeline: pipeline)
end
end
@@ -5585,7 +5638,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:model) { create(:ci_build, user: create(:user)) }
+ let!(:model) { create(:ci_build, user: create(:user), pipeline: pipeline) }
let!(:parent) { model.user }
end
@@ -5595,7 +5648,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when given new job variables' do
context 'when the cloned build has an action' do
it 'applies the new job variables' do
- build = create(:ci_build, :actionable)
+ build = create(:ci_build, :actionable, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
create(:ci_job_variable, job: build, key: 'OLD_KEY', value: 'i will not live for long')
@@ -5614,7 +5667,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when the cloned build does not have an action' do
it 'applies the old job variables' do
- build = create(:ci_build)
+ build = create(:ci_build, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
new_build = build.clone(
@@ -5632,7 +5685,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when not given new job variables' do
it 'applies the old job variables' do
- build = create(:ci_build)
+ build = create(:ci_build, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
new_build = build.clone(current_user: user)
@@ -5646,14 +5699,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#test_suite_name' do
- let(:build) { create(:ci_build, name: 'test') }
+ let(:build) { create(:ci_build, name: 'test', pipeline: pipeline) }
it 'uses the group name for test suite name' do
expect(build.test_suite_name).to eq('test')
end
context 'when build is part of parallel build' do
- let(:build) { create(:ci_build, name: 'build 1/2') }
+ let(:build) { create(:ci_build, name: 'build 1/2', pipeline: pipeline) }
it 'uses the group name for test suite name' do
expect(build.test_suite_name).to eq('build')
@@ -5661,7 +5714,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is part of matrix build' do
- let!(:matrix_build) { create(:ci_build, :matrix) }
+ let!(:matrix_build) { create(:ci_build, :matrix, pipeline: pipeline) }
it 'uses the job name for the test suite' do
expect(matrix_build.test_suite_name).to eq(matrix_build.name)
@@ -5672,7 +5725,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#runtime_hooks' do
let(:build1) do
FactoryBot.build(:ci_build,
- options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } },
+ pipeline: pipeline)
end
subject(:runtime_hooks) { build1.runtime_hooks }
@@ -5687,7 +5741,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'partitioning', :ci_partitionable do
include Ci::PartitioningHelpers
- let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { FactoryBot.build(:ci_build, pipeline: new_pipeline) }
before do
@@ -5711,7 +5765,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'assigning token', :ci_partitionable do
include Ci::PartitioningHelpers
- let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, pipeline: new_pipeline) }
before do
@@ -5741,4 +5795,118 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
expect { build.remove_token! }.not_to change(build, :token)
end
end
+
+ describe 'metadata partitioning', :ci_partitioning do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
+
+ let(:build) do
+ FactoryBot.build(:ci_build, pipeline: pipeline)
+ end
+
+ it 'creates the metadata record and assigns its partition' do
+ # The record is initialized by the factory calling metadatable setters
+ build.metadata = nil
+
+ expect(build.metadata).to be_nil
+
+ expect(build.save!).to be_truthy
+
+ expect(build.metadata).to be_present
+ expect(build.metadata).to be_valid
+ expect(build.metadata.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+
+ describe 'secrets management id_tokens usage data' do
+ context 'when ID tokens are defined' do
+ context 'on create' do
+ let(:ci_build) { FactoryBot.build(:ci_build, user: user, id_tokens: { 'ID_TOKEN_1' => { aud: 'developers' } }) }
+
+ it 'tracks RedisHLL event with user_id' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ .with('i_ci_secrets_management_id_tokens_build_created', values: user.id)
+
+ ci_build.save!
+ end
+
+ it 'tracks Snowplow event with RedisHLL context' do
+ params = {
+ category: described_class.to_s,
+ action: 'create_id_tokens',
+ namespace: ci_build.namespace,
+ user: user,
+ label: 'redis_hll_counters.ci_secrets_management.i_ci_secrets_management_id_tokens_build_created_monthly',
+ ultimate_namespace_id: ci_build.namespace.root_ancestor.id,
+ context: [Gitlab::Tracking::ServicePingContext.new(
+ data_source: :redis_hll,
+ event: 'i_ci_secrets_management_id_tokens_build_created'
+ ).to_context.to_json]
+ }
+
+ ci_build.save!
+ expect_snowplow_event(**params)
+ end
+ end
+
+ context 'on update' do
+ let_it_be(:ci_build) { create(:ci_build, user: user, id_tokens: { 'ID_TOKEN_1' => { aud: 'developers' } }) }
+
+ it 'does not track RedisHLL event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ ci_build.success
+ end
+
+ it 'does not track Snowplow event' do
+ ci_build.success
+
+ expect_no_snowplow_event
+ end
+ end
+ end
+
+ context 'when ID tokens are not defined' do
+ let(:ci_build) { FactoryBot.build(:ci_build, user: user) }
+
+ context 'on create' do
+ it 'does not track RedisHLL event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ ci_build.save!
+ end
+
+ it 'does not track Snowplow event' do
+ ci_build.save!
+ expect_no_snowplow_event
+ end
+ end
+ end
+ end
+
+ describe 'job artifact associations' do
+ Ci::JobArtifact.file_types.each do |type, _|
+ method = "job_artifacts_#{type}"
+
+ describe "##{method}" do
+ subject { build.send(method) }
+
+ context "when job has an artifact of type #{type}" do
+ let!(:artifact) do
+ create(
+ :ci_job_artifact,
+ job: build,
+ file_type: type,
+ file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym]
+ )
+ end
+
+ it { is_expected.to eq(artifact) }
+ end
+
+ context "when job has no artifact of type #{type}" do
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index fc5a9c879f6..e73319cfcd7 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -2,10 +2,13 @@
require 'spec_helper'
-RSpec.describe Ci::GroupVariable do
- subject { build(:ci_group_variable) }
+RSpec.describe Ci::GroupVariable, feature_category: :pipeline_authoring do
+ let_it_be_with_refind(:group) { create(:group) }
+
+ subject { build(:ci_group_variable, group: group) }
it_behaves_like "CI variable"
+ it_behaves_like 'includes Limitable concern'
it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Ci::Maskable) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a1fd51f60ea..e94445f17cd 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifact do
+RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
let(:artifact) { create(:ci_job_artifact, :archive) }
describe "Associations" do
@@ -27,6 +27,29 @@ RSpec.describe Ci::JobArtifact do
subject { build(:ci_job_artifact, :archive, job: job, size: 107464) }
end
+ describe 'after_create_commit callback' do
+ it 'logs the job artifact create' do
+ artifact = build(:ci_job_artifact, file_type: 3, size: 8888, file_format: 2, locked: 1)
+
+ expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_created) do |record|
+ expect(record.size).to eq(artifact.size)
+ expect(record.file_type).to eq(artifact.file_type)
+ expect(record.file_format).to eq(artifact.file_format)
+ expect(record.locked).to eq(artifact.locked)
+ end
+
+ artifact.save!
+ end
+ end
+
+ describe 'after_destroy_commit callback' do
+ it 'logs the job artifact destroy' do
+ expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_deleted).with(artifact, :log_destroy)
+
+ artifact.destroy!
+ end
+ end
+
describe '.not_expired' do
it 'returns artifacts that have not expired' do
_expired_artifact = create(:ci_job_artifact, :expired)
@@ -770,4 +793,10 @@ RSpec.describe Ci::JobArtifact do
end
end
end
+
+ describe '#filename' do
+ subject { artifact.filename }
+
+ it { is_expected.to eq(artifact.file.filename) }
+ end
end
diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb
index 45083d64393..3a2673c7c26 100644
--- a/spec/models/ci/job_token/allowlist_spec.rb
+++ b/spec/models/ci/job_token/allowlist_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do
+ include Ci::JobTokenScopeHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:source_project) { create(:project) }
@@ -24,11 +25,11 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
context 'when projects are added to the scope' do
- include_context 'with scoped projects'
+ include_context 'with a project in each allowlist'
where(:direction, :additional_project) do
- :outbound | ref(:outbound_scoped_project)
- :inbound | ref(:inbound_scoped_project)
+ :outbound | ref(:outbound_allowlist_project)
+ :inbound | ref(:inbound_allowlist_project)
end
with_them do
@@ -39,6 +40,26 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
end
+ describe 'add!' do
+ let_it_be(:added_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { allowlist.add!(added_project, user: user) }
+
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
+
+ it 'adds the project' do
+ subject
+
+ expect(allowlist.projects).to contain_exactly(source_project, added_project)
+ expect(subject.added_by_id).to eq(user.id)
+ expect(subject.source_project_id).to eq(source_project.id)
+ expect(subject.target_project_id).to eq(added_project.id)
+ end
+ end
+ end
+
describe '#includes?' do
subject { allowlist.includes?(includes_project) }
@@ -57,16 +78,16 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
end
- context 'with scoped projects' do
- include_context 'with scoped projects'
+ context 'with a project in each allowlist' do
+ include_context 'with a project in each allowlist'
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(:inbound_allowlist_project) | :outbound | false
+ ref(:inbound_allowlist_project) | :inbound | true
+ ref(:outbound_allowlist_project) | :outbound | true
+ ref(:outbound_allowlist_project) | :inbound | false
ref(:unscoped_project1) | :outbound | false
ref(:unscoped_project1) | :inbound | false
ref(:unscoped_project2) | :outbound | false
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 91491733c44..310f9b550f4 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -18,15 +18,40 @@ RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_int
describe 'unique index' do
let!(:link) { create(:ci_job_token_project_scope_link) }
- it 'raises an error' do
+ it 'raises an error, when not unique' do
expect do
create(:ci_job_token_project_scope_link,
source_project: link.source_project,
- target_project: link.target_project)
+ target_project: link.target_project,
+ direction: link.direction)
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end
+ describe '.create' do
+ let_it_be(:target) { create(:project) }
+ let(:new_link) { described_class.create(source_project: project, target_project: target) } # rubocop:disable Rails/SaveBang
+
+ context 'when there are more than PROJECT_LINK_DIRECTIONAL_LIMIT existing links' do
+ before do
+ create_list(:ci_job_token_project_scope_link, 5, source_project: project)
+ stub_const("#{described_class}::PROJECT_LINK_DIRECTIONAL_LIMIT", 3)
+ end
+
+ it 'invalidates new links and prevents them from being created' do
+ expect { new_link }.not_to change { described_class.count }
+ expect(new_link).not_to be_persisted
+ expect(new_link.errors.full_messages)
+ .to include('Source project exceeds the allowable number of project links in this direction')
+ end
+
+ it 'does not invalidate existing links' do
+ expect(described_class.count).to be > described_class::PROJECT_LINK_DIRECTIONAL_LIMIT
+ expect(described_class.all).to all(be_valid)
+ end
+ end
+ end
+
describe 'validations' do
it 'must have a source project', :aggregate_failures do
link = build(:ci_job_token_project_scope_link, source_project: nil)
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 37c56973506..9ae061a3702 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -2,78 +2,171 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do
- let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) }
+RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, factory_default: :keep do
+ include Ci::JobTokenScopeHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create_default(:project) }
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:namespace) { create_default(:namespace) }
+
+ let_it_be(:source_project) do
+ create(:project,
+ ci_outbound_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ )
+ end
+
+ let(:current_project) { source_project }
- let(:scope) { described_class.new(source_project) }
+ let(:scope) { described_class.new(current_project) }
- describe '#all_projects' do
- subject(:all_projects) { scope.all_projects }
+ describe '#outbound_projects' do
+ subject { scope.outbound_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(source_project)
+ expect(subject).to contain_exactly(current_project)
end
end
context 'when projects are added to the scope' do
- include_context 'with scoped projects'
+ include_context 'with accessible and inaccessible projects'
it 'returns all projects that can be accessed from a given scope' do
- expect(subject).to contain_exactly(source_project, outbound_scoped_project)
+ expect(subject).to contain_exactly(current_project, outbound_allowlist_project, fully_accessible_project)
end
end
end
- describe '#allows?' do
- subject { scope.allows?(includes_project) }
+ describe '#inbound_projects' do
+ subject { scope.inbound_projects }
- context 'without scoped projects' do
- context 'when self referential' do
- let(:includes_project) { source_project }
+ context 'when no projects are added to the scope' do
+ it 'returns the project defining the scope' do
+ expect(subject).to contain_exactly(current_project)
+ end
+ end
+
+ context 'when projects are added to the scope' do
+ include_context 'with accessible and inaccessible projects'
- it { is_expected.to be_truthy }
+ it 'returns all projects that can be accessed from a given scope' do
+ expect(subject).to contain_exactly(current_project, inbound_allowlist_project)
end
end
+ end
+
+ describe 'add!' do
+ let_it_be(:new_project) { create(:project) }
- context 'with scoped projects' do
- include_context 'with scoped projects'
+ subject { scope.add!(new_project, direction: direction, user: user) }
- context 'when project is in outbound scope' do
- let(:includes_project) { outbound_scoped_project }
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
- it { is_expected.to be_truthy }
+ it 'adds the project' do
+ subject
+
+ expect(scope.send("#{direction}_projects")).to contain_exactly(current_project, new_project)
end
+ end
- context 'when project is in inbound scope' do
- let(:includes_project) { inbound_scoped_project }
+ # Context and before block can go away leaving just the example in 16.0
+ context 'with inbound only enabled' do
+ before do
+ project.ci_cd_settings.update!(job_token_scope_enabled: false)
+ end
- it { is_expected.to be_falsey }
+ it 'provides access' do
+ expect do
+ scope.add!(new_project, direction: :inbound, user: user)
+ end.to change { described_class.new(new_project).accessible?(current_project) }.from(false).to(true)
end
+ end
+ end
+
+ RSpec.shared_examples 'enforces outbound scope only' do
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | true
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ end
- context 'when project is linked to a different project' do
- let(:includes_project) { unscoped_project1 }
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ describe 'accessible?' do
+ subject { scope.accessible?(accessed_project) }
+
+ context 'with inbound and outbound scopes enabled' do
+ context 'when inbound and outbound access setup' do
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | false
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ end
+
+ with_them do
+ it 'allows self and projects allowed from both directions' do
+ is_expected.to eq(result)
+ end
+ end
+ end
+ end
- it { is_expected.to be_falsey }
+ context 'with inbound scope enabled and outbound scope disabled' do
+ before do
+ accessed_project.update!(ci_inbound_job_token_scope_enabled: true)
+ current_project.update!(ci_outbound_job_token_scope_enabled: false)
end
- context 'when project is unlinked to a project' do
- let(:includes_project) { unscoped_project2 }
+ include_context 'with accessible and inaccessible projects'
- it { is_expected.to be_falsey }
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | false
+ ref(:inbound_accessible_project) | true
+ ref(:fully_accessible_project) | true
end
- context 'when project scope setting is disabled' do
- let(:includes_project) { unscoped_project1 }
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+ end
- before do
- source_project.ci_outbound_job_token_scope_enabled = false
- end
+ context 'with inbound scope disabled and outbound scope enabled' do
+ before do
+ accessed_project.update!(ci_inbound_job_token_scope_enabled: false)
+ current_project.update!(ci_outbound_job_token_scope_enabled: true)
+ end
- it 'considers any project to be part of the scope' do
- expect(subject).to be_truthy
- end
+ include_examples 'enforces outbound scope only'
+ end
+
+ context 'when inbound scope flag disabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: false)
end
+
+ include_examples 'enforces outbound scope only'
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5888f9d109c..61422978df7 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -226,9 +226,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline') }
context 'when name exists' do
- let(:name) { 'build Pipeline' }
+ let(:name) { 'Build pipeline' }
- it 'performs case insensitive compare' do
+ it 'performs exact compare' do
is_expected.to contain_exactly(pipeline1)
end
end
@@ -1070,296 +1070,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
- describe '#predefined_variables' do
- subject { pipeline.predefined_variables }
-
- let(:pipeline) { build(:ci_empty_pipeline, :created) }
-
- it 'includes all predefined variables in a valid order' do
- keys = subject.map { |variable| variable[:key] }
-
- expect(keys).to eq %w[
- CI_PIPELINE_IID
- CI_PIPELINE_SOURCE
- CI_PIPELINE_CREATED_AT
- CI_COMMIT_SHA
- CI_COMMIT_SHORT_SHA
- CI_COMMIT_BEFORE_SHA
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_COMMIT_BRANCH
- CI_COMMIT_MESSAGE
- CI_COMMIT_TITLE
- CI_COMMIT_DESCRIPTION
- CI_COMMIT_REF_PROTECTED
- CI_COMMIT_TIMESTAMP
- CI_COMMIT_AUTHOR
- CI_BUILD_REF
- CI_BUILD_BEFORE_SHA
- CI_BUILD_REF_NAME
- CI_BUILD_REF_SLUG
- ]
- end
-
- context 'when merge request is present' do
- let_it_be(:assignees) { create_list(:user, 2) }
- let_it_be(:milestone) { create(:milestone, project: project) }
- let_it_be(:labels) { create_list(:label, 2) }
-
- let(:merge_request) do
- create(:merge_request, :simple,
- source_project: project,
- target_project: project,
- assignees: assignees,
- milestone: milestone,
- labels: labels)
- end
-
- context 'when pipeline for merge request is created' do
- let(:pipeline) do
- create(:ci_pipeline, :detached_merge_request_pipeline,
- ci_ref_presence: false,
- user: user,
- merge_request: merge_request)
- end
-
- before do
- project.add_developer(user)
- end
-
- it 'exposes merge request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
- 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
- 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
- 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
- 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
- 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(merge_request.target_project, merge_request.target_branch).to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '',
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '',
- 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
- 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
- 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
- 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached',
- 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
- end
-
- it 'exposes diff variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
- 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
- end
-
- context 'without assignee' do
- let(:assignees) { [] }
-
- it 'does not expose assignee variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
- end
- end
-
- context 'without milestone' do
- let(:milestone) { nil }
-
- it 'does not expose milestone variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
- end
- end
-
- context 'without labels' do
- let(:labels) { [] }
-
- it 'does not expose labels variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
- end
- end
- end
-
- context 'when pipeline on branch is created' do
- let(:pipeline) do
- create(:ci_pipeline, project: project, user: user, ref: 'feature')
- end
-
- context 'when a merge request is created' do
- before do
- merge_request
- end
-
- context 'when user has access to project' do
- before do
- project.add_developer(user)
- end
-
- it 'merge request references are returned matching the pipeline' do
- expect(subject.to_hash).to include(
- 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
- end
- end
-
- context 'when user does not have access to project' do
- it 'CI_OPEN_MERGE_REQUESTS is not returned' do
- expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
- end
- end
- end
-
- context 'when no a merge request is created' do
- it 'CI_OPEN_MERGE_REQUESTS is not returned' do
- expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
- end
- end
- end
-
- context 'with merged results' do
- let(:pipeline) do
- create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request)
- end
-
- it 'exposes merge request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
- 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
- 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
- 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
- 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
- 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(merge_request.target_project, merge_request.target_branch).to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
- 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
- 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
- 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
- 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result')
- end
-
- it 'exposes diff variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
- 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
- end
- end
- end
-
- context 'when source is external pull request' do
- let(:pipeline) do
- create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
- end
-
- let(:pull_request) { create(:external_pull_request, project: project) }
-
- it 'exposes external pull request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
- )
- end
- end
-
- describe 'variable CI_KUBERNETES_ACTIVE' do
- context 'when pipeline.has_kubernetes_active? is true' do
- before do
- allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
- end
-
- it "is included with value 'true'" do
- expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
- end
- end
-
- context 'when pipeline.has_kubernetes_active? is false' do
- before do
- allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
- end
-
- it 'is not included' do
- expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
- end
- end
- end
-
- describe 'variable CI_GITLAB_FIPS_MODE' do
- context 'when FIPS flag is enabled' do
- before do
- allow(Gitlab::FIPS).to receive(:enabled?).and_return(true)
- end
-
- it "is included with value 'true'" do
- expect(subject.to_hash).to include('CI_GITLAB_FIPS_MODE' => 'true')
- end
- end
-
- context 'when FIPS flag is disabled' do
- before do
- allow(Gitlab::FIPS).to receive(:enabled?).and_return(false)
- end
-
- it 'is not included' do
- expect(subject.to_hash).not_to have_key('CI_GITLAB_FIPS_MODE')
- end
- end
- end
-
- context 'when tag is not found' do
- let(:pipeline) do
- create(:ci_pipeline, project: project, ref: 'not_found_tag', tag: true)
- end
-
- it 'does not expose tag variables' do
- expect(subject.to_hash.keys)
- .not_to include(
- 'CI_COMMIT_TAG',
- 'CI_COMMIT_TAG_MESSAGE',
- 'CI_BUILD_TAG'
- )
- end
- end
-
- context 'without a commit' do
- let(:pipeline) { build(:ci_empty_pipeline, :created, sha: nil) }
-
- it 'does not expose commit variables' do
- expect(subject.to_hash.keys)
- .not_to include(
- 'CI_COMMIT_SHA',
- 'CI_COMMIT_SHORT_SHA',
- 'CI_COMMIT_BEFORE_SHA',
- 'CI_COMMIT_REF_NAME',
- 'CI_COMMIT_REF_SLUG',
- 'CI_COMMIT_BRANCH',
- 'CI_COMMIT_TAG',
- 'CI_COMMIT_MESSAGE',
- 'CI_COMMIT_TITLE',
- 'CI_COMMIT_DESCRIPTION',
- 'CI_COMMIT_REF_PROTECTED',
- 'CI_COMMIT_TIMESTAMP',
- 'CI_COMMIT_AUTHOR')
- end
- end
- end
-
describe '#protected_ref?' do
let(:pipeline) { build(:ci_empty_pipeline, :created) }
@@ -5664,6 +5374,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '#merge_request_diff' do
+ context 'when the pipeline has no merge request' do
+ it 'is nil' do
+ pipeline = build(:ci_empty_pipeline)
+
+ expect(pipeline.merge_request_diff).to be_nil
+ end
+ end
+
+ context 'when the pipeline has a merge request' do
+ context 'when the pipeline is a merged result pipeline' do
+ it 'returns the diff for the source sha' do
+ pipeline = create(:ci_pipeline, :merged_result_pipeline)
+
+ expect(pipeline.merge_request_diff.head_commit_sha).to eq(pipeline.source_sha)
+ end
+ end
+
+ context 'when the pipeline is not a merged result pipeline' do
+ it 'returns the diff for the pipeline sha' do
+ pipeline = create(:ci_pipeline, merge_request: create(:merge_request))
+
+ expect(pipeline.merge_request_diff.head_commit_sha).to eq(pipeline.sha)
+ end
+ end
+ end
+ end
+
describe 'partitioning' do
let(:pipeline) { build(:ci_pipeline, partition_id: nil) }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 07fac4ee2f7..db22d8f3a6c 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Processable do
+RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -83,7 +83,7 @@ RSpec.describe Ci::Processable do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
- metadata runner_session trace_chunks upstream_pipeline_id
+ metadata runner_machine_id runner_machine runner_session trace_chunks upstream_pipeline_id
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
@@ -287,6 +287,12 @@ RSpec.describe Ci::Processable do
end
end
+ context 'when the processable is a bridge' do
+ subject(:processable) { create(:ci_bridge, pipeline: pipeline) }
+
+ it_behaves_like 'retryable processable'
+ end
+
context 'when the processable is a build' do
subject(:processable) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/models/ci/runner_machine_spec.rb b/spec/models/ci/runner_machine_spec.rb
index e39f987110f..d0979d8a485 100644
--- a/spec/models/ci/runner_machine_spec.rb
+++ b/spec/models/ci/runner_machine_spec.rb
@@ -6,11 +6,14 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
it_behaves_like 'having unique enum values'
it { is_expected.to belong_to(:runner) }
+ it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
+ it { is_expected.to have_many(:build_metadata) }
+ it { is_expected.to have_many(:builds).through(:build_metadata) }
describe 'validation' do
it { is_expected.to validate_presence_of(:runner) }
- it { is_expected.to validate_presence_of(:machine_xid) }
- it { is_expected.to validate_length_of(:machine_xid).is_at_most(64) }
+ it { is_expected.to validate_presence_of(:system_xid) }
+ it { is_expected.to validate_length_of(:system_xid).is_at_most(64) }
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
it { is_expected.to validate_length_of(:revision).is_at_most(255) }
it { is_expected.to validate_length_of(:platform).is_at_most(255) }
@@ -37,10 +40,11 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
describe '.stale', :freeze_time do
subject { described_class.stale.ids }
- let!(:runner_machine1) { create(:ci_runner_machine, created_at: 8.days.ago, contacted_at: 7.days.ago) }
- let!(:runner_machine2) { create(:ci_runner_machine, created_at: 7.days.ago, contacted_at: nil) }
- let!(:runner_machine3) { create(:ci_runner_machine, created_at: 5.days.ago, contacted_at: nil) }
- let!(:runner_machine4) do
+ let!(:runner_machine1) { create(:ci_runner_machine, :stale) }
+ let!(:runner_machine2) { create(:ci_runner_machine, :stale, contacted_at: nil) }
+ let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
+ let!(:runner_machine4) { create(:ci_runner_machine, created_at: 5.days.ago) }
+ let!(:runner_machine5) do
create(:ci_runner_machine, created_at: (7.days - 1.second).ago, contacted_at: (7.days - 1.second).ago)
end
@@ -48,4 +52,146 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
is_expected.to match_array([runner_machine1.id, runner_machine2.id])
end
end
+
+ describe '#heartbeat', :freeze_time do
+ let(:runner_machine) { create(:ci_runner_machine) }
+ let(:executor) { 'shell' }
+ let(:version) { '15.0.1' }
+ let(:values) do
+ {
+ ip_address: '8.8.8.8',
+ architecture: '18-bit',
+ config: { gpus: "all" },
+ executor: executor,
+ version: version
+ }
+ end
+
+ subject(:heartbeat) do
+ runner_machine.heartbeat(values)
+ end
+
+ context 'when database was updated recently' do
+ before do
+ runner_machine.contacted_at = Time.current
+ end
+
+ it 'schedules version update' do
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+
+ heartbeat
+
+ expect(runner_machine.runner_version).to be_nil
+ end
+
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+ end
+
+ context 'with only ip_address specified' do
+ let(:values) do
+ { ip_address: '1.1.1.1' }
+ end
+
+ it 'updates only ip_address' do
+ attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ end
+
+ heartbeat
+ end
+ end
+ end
+
+ context 'when database was not updated recently' do
+ before do
+ runner_machine.contacted_at = 2.hours.ago
+
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+ end
+
+ context 'with invalid runner_machine' do
+ before do
+ runner_machine.runner = nil
+ end
+
+ it 'still updates redis cache and database' do
+ expect(runner_machine).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ context 'with unchanged runner_machine version' do
+ let(:runner_machine) { create(:ci_runner_machine, version: version) }
+
+ it 'does not schedule ci_runner_versions update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
+
+ it 'updates redis cache and database' do
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
+
+ it 'updates with expected executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
+
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
+ end
+ end
+
+ context "with an unknown executor type" do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
+ end
+ end
+ end
+
+ def expect_redis_update
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, anything, any_args).and_call_original
+ end
+ end
+
+ def does_db_update
+ expect { heartbeat }.to change { runner_machine.reload.read_attribute(:contacted_at) }
+ .and change { runner_machine.reload.read_attribute(:architecture) }
+ .and change { runner_machine.reload.read_attribute(:config) }
+ .and change { runner_machine.reload.read_attribute(:executor_type) }
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index b7c7b67b98f..01d5fe7f90b 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, feature_category: :runner do
+RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
include StubGitlabCalls
it_behaves_like 'having unique enum values'
@@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) }
+ it { is_expected.to validate_presence_of(:registration_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@@ -259,16 +260,16 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
describe '.belonging_to_project' do
- it 'returns the specific project runner' do
+ it 'returns the project runner' do
# own
- specific_project = create(:project)
- specific_runner = create(:ci_runner, :project, projects: [specific_project])
+ own_project = create(:project)
+ own_runner = create(:ci_runner, :project, projects: [own_project])
# other
other_project = create(:project)
create(:ci_runner, :project, projects: [other_project])
- expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
+ expect(described_class.belonging_to_project(own_project.id)).to eq [own_runner]
end
end
@@ -285,7 +286,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
subject(:result) { described_class.belonging_to_parent_group_of_project(project_id) }
- it 'returns the specific group runner' do
+ it 'returns the group runner' do
expect(result).to contain_exactly(runner1)
end
@@ -339,7 +340,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe '.owned_or_instance_wide' do
subject { described_class.owned_or_instance_wide(project.id) }
- it 'returns a globally shared, a project specific and a group specific runner' do
+ it 'returns a shared, project and group runner' do
is_expected.to contain_exactly(group_runner, project_runner, shared_runner)
end
end
@@ -352,7 +353,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
project_runner
end
- it 'returns a globally shared and a group specific runner' do
+ it 'returns a globally shared and a group runner' do
is_expected.to contain_exactly(group_runner, shared_runner)
end
end
@@ -382,7 +383,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
context 'with group runners disabled' do
let(:group_runners_enabled) { false }
- it 'returns only the project specific runner' do
+ it 'returns only the project runner' do
is_expected.to contain_exactly(project_runner)
end
end
@@ -390,7 +391,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
context 'with group runners enabled' do
let(:group_runners_enabled) { true }
- it 'returns a project specific and a group specific runner' do
+ it 'returns a project runner and a group runner' do
is_expected.to contain_exactly(group_runner, project_runner)
end
end
@@ -404,7 +405,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
project_runner
end
- it 'returns a group specific runner' do
+ it 'returns a group runner' do
is_expected.to contain_exactly(group_runner)
end
end
@@ -1737,6 +1738,40 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
end
+ describe '#short_sha' do
+ subject(:short_sha) { runner.short_sha }
+
+ context 'when registered via command-line' do
+ let(:runner) { create(:ci_runner) }
+
+ specify { expect(runner.token).not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ end
+
+ context 'when creating new runner via UI' do
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
+ specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ end
+ end
+
+ describe '#token' do
+ subject(:token) { runner.token }
+
+ context 'when runner is registered' do
+ let(:runner) { create(:ci_runner) }
+
+ it { is_expected.not_to start_with('glrt-') }
+ end
+
+ context 'when runner is created via UI' do
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
+ it { is_expected.to start_with('glrt-') }
+ end
+ end
+
describe '#token_expires_at', :freeze_time do
shared_examples 'expiring token' do |interval:|
it 'expires' do
@@ -1915,7 +1950,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
end
- describe '#with_upgrade_status' do
+ describe '.with_upgrade_status' do
subject { described_class.with_upgrade_status(upgrade_status) }
let_it_be(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
@@ -1923,12 +1958,12 @@ RSpec.describe Ci::Runner, feature_category: :runner do
let_it_be(:runner_14_1_1) { create(:ci_runner, version: '14.1.1') }
let_it_be(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :available) }
let_it_be(:runner_version_14_1_0) { create(:ci_runner_version, version: '14.1.0', status: :recommended) }
- let_it_be(:runner_version_14_1_1) { create(:ci_runner_version, version: '14.1.1', status: :not_available) }
+ let_it_be(:runner_version_14_1_1) { create(:ci_runner_version, version: '14.1.1', status: :unavailable) }
- context ':not_available' do
- let(:upgrade_status) { :not_available }
+ context ':unavailable' do
+ let(:upgrade_status) { :unavailable }
- it 'returns runners whose version is assigned :not_available' do
+ it 'returns runners whose version is assigned :unavailable' do
is_expected.to contain_exactly(runner_14_1_1)
end
end
diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb
index dfaa2201859..51a2f14c57c 100644
--- a/spec/models/ci/runner_version_spec.rb
+++ b/spec/models/ci/runner_version_spec.rb
@@ -3,33 +3,35 @@
require 'spec_helper'
RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
- let_it_be(:runner_version_recommended) do
+ let_it_be(:runner_version_upgrade_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)
+ let_it_be(:runner_version_upgrade_unavailable) do
+ create(:ci_runner_version, version: 'abc123', status: :unavailable)
end
+ it { is_expected.to have_many(:runner_machines).with_foreign_key(:version) }
+
it_behaves_like 'having unique enum values'
- describe '.not_available' do
- subject { described_class.not_available }
+ describe '.unavailable' do
+ subject { described_class.unavailable }
- it { is_expected.to match_array([runner_version_not_available]) }
+ it { is_expected.to match_array([runner_version_upgrade_unavailable]) }
end
describe '.potentially_outdated' do
subject { described_class.potentially_outdated }
let_it_be(:runner_version_nil) { create(:ci_runner_version, version: 'abc345', status: nil) }
- let_it_be(:runner_version_available) do
+ let_it_be(:runner_version_upgrade_available) do
create(:ci_runner_version, version: 'abc456', status: :available)
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]
+ [runner_version_nil, runner_version_upgrade_unavailable, runner_version_upgrade_available]
)
end
end
diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb
index 1a5ea044ba3..7f254bd235c 100644
--- a/spec/models/ci/running_build_spec.rb
+++ b/spec/models/ci/running_build_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Ci::RunningBuild, feature_category: :continuous_integration do
end
end
- context 'when build has been picked by a specific runner' do
+ context 'when build has been picked by a project runner' do
let(:runner) { create(:ci_runner, :project) }
it 'raises an error' do
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
index 87077fe2db1..38ae908fb00 100644
--- a/spec/models/ci/secure_file_spec.rb
+++ b/spec/models/ci/secure_file_spec.rb
@@ -101,6 +101,11 @@ RSpec.describe Ci::SecureFile do
file = build(:ci_secure_file, name: 'file1.tar.gz')
expect(file.file_extension).to eq('gz')
end
+
+ it 'returns nil if there is no file extension' do
+ file = build(:ci_secure_file, name: 'file1')
+ expect(file.file_extension).to be nil
+ end
end
describe '#metadata_parsable?' do
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 8517e583ec7..5eef719ae0c 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Trigger do
+RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
let(:project) { create :project }
describe 'associations' do
@@ -86,4 +86,40 @@ RSpec.describe Ci::Trigger do
let!(:model) { create(:ci_trigger, project: parent) }
end
end
+
+ describe 'encrypted_token' do
+ context 'when token is not provided' do
+ it 'encrypts the generated token' do
+ trigger = create(:ci_trigger_without_token, project: project)
+
+ expect(trigger.token).not_to be_nil
+ expect(trigger.encrypted_token).not_to be_nil
+ expect(trigger.encrypted_token_iv).not_to be_nil
+
+ expect(trigger.reload.encrypted_token_tmp).to eq(trigger.token)
+ end
+ end
+
+ context 'when token is provided' do
+ it 'encrypts the given token' do
+ trigger = create(:ci_trigger, project: project)
+
+ expect(trigger.token).not_to be_nil
+ expect(trigger.encrypted_token).not_to be_nil
+ expect(trigger.encrypted_token_iv).not_to be_nil
+
+ expect(trigger.reload.encrypted_token_tmp).to eq(trigger.token)
+ end
+ end
+
+ context 'when token is being updated' do
+ it 'encrypts the given token' do
+ trigger = create(:ci_trigger, project: project, token: "token")
+ expect { trigger.update!(token: "new token") }
+ .to change { trigger.encrypted_token }
+ .and change { trigger.encrypted_token_iv }
+ .and change { trigger.encrypted_token_tmp }.from("token").to("new token")
+ end
+ end
+ end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 5f2b5971508..ce64b3ea158 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -2,10 +2,13 @@
require 'spec_helper'
-RSpec.describe Ci::Variable do
- subject { build(:ci_variable) }
+RSpec.describe Ci::Variable, feature_category: :pipeline_authoring do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ subject { build(:ci_variable, project: project) }
it_behaves_like "CI variable"
+ it_behaves_like 'includes Limitable concern'
describe 'validations' do
it { is_expected.to include_module(Presentable) }
diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb
index f73db713791..e59730792b8 100644
--- a/spec/models/ci_platform_metric_spec.rb
+++ b/spec/models/ci_platform_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CiPlatformMetric do
+RSpec.describe CiPlatformMetric, feature_category: :continuous_integration do
subject { build(:ci_platform_metric) }
it_behaves_like 'a BulkInsertSafe model', CiPlatformMetric do
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
deleted file mode 100644
index 427a99efadd..00000000000
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ /dev/null
@@ -1,157 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CertManager do
- let(:cert_manager) { create(:clusters_applications_cert_manager) }
-
- include_examples 'cluster application core specs', :clusters_applications_cert_manager
- include_examples 'cluster application status specs', :clusters_applications_cert_manager
- include_examples 'cluster application version specs', :clusters_applications_cert_manager
- include_examples 'cluster application initial status specs'
-
- describe 'default values' do
- it { expect(cert_manager.version).to eq(described_class::VERSION) }
- it { expect(cert_manager.email).to eq("admin@example.com") }
- end
-
- describe '#can_uninstall?' do
- subject { cert_manager.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#install_command' do
- let(:cert_email) { 'admin@example.com' }
-
- let(:cluster_issuer_file) do
- file_contents = <<~EOF
- ---
- apiVersion: certmanager.k8s.io/v1alpha1
- kind: ClusterIssuer
- metadata:
- name: letsencrypt-prod
- spec:
- acme:
- server: https://acme-v02.api.letsencrypt.org/directory
- email: #{cert_email}
- privateKeySecretRef:
- name: letsencrypt-prod
- http01: {}
- EOF
-
- { "cluster_issuer.yaml": file_contents }
- end
-
- subject { cert_manager.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with cert_manager arguments' do
- expect(subject.name).to eq('certmanager')
- expect(subject.chart).to eq('certmanager/cert-manager')
- expect(subject.repository).to eq('https://charts.jetstack.io')
- expect(subject.version).to eq('v0.10.1')
- expect(subject).to be_rbac
- expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
- expect(subject.preinstall).to eq(
- [
- 'kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml',
- 'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
- ])
- expect(subject.postinstall).to eq(
- [
- "for i in $(seq 1 90); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
- ])
- end
-
- context 'for a specific user' do
- let(:cert_email) { 'abc@xyz.com' }
-
- before do
- cert_manager.email = cert_email
- end
-
- it 'uses their email to register issuer with certificate provider' do
- expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
- end
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- cert_manager.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:cert_manager) { create(:clusters_applications_cert_manager, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('v0.10.1')
- end
- end
- end
-
- describe '#uninstall_command' do
- subject { cert_manager.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it 'is initialized with cert_manager arguments' do
- expect(subject.name).to eq('certmanager')
- expect(subject).to be_rbac
- expect(subject.files).to eq(cert_manager.files)
- end
-
- it 'specifies a post delete command to remove custom resource definitions' do
- expect(subject.postdelete).to eq(
- [
- 'kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found',
- 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
- ])
- end
-
- context 'secret key name is not found' do
- before do
- allow(File).to receive(:read).and_call_original
- expect(File).to receive(:read)
- .with(Rails.root.join('vendor', 'cert_manager', 'cluster_issuer.yaml'))
- .and_return('key: value')
- end
-
- it 'does not try and delete the secret' do
- expect(subject.postdelete).to eq(
- [
- 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
- ])
- end
- end
- end
-
- describe '#files' do
- let(:application) { cert_manager }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes cert_manager specific keys in the values.yaml file' do
- expect(values).to include('ingressShim')
- end
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:email) }
- end
-end
diff --git a/spec/models/clusters/applications/cilium_spec.rb b/spec/models/clusters/applications/cilium_spec.rb
deleted file mode 100644
index 8b01502d5c0..00000000000
--- a/spec/models/clusters/applications/cilium_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Cilium do
- let(:cilium) { create(:clusters_applications_cilium) }
-
- include_examples 'cluster application core specs', :clusters_applications_cilium
- include_examples 'cluster application status specs', :clusters_applications_cilium
- include_examples 'cluster application initial status specs'
-
- describe '#allowed_to_uninstall?' do
- subject { cilium.allowed_to_uninstall? }
-
- it { is_expected.to be false }
- end
-end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index be64d72e031..2a2e2899d24 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
+RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
+feature_category: :kubernetes_management do
include ReactiveCachingHelpers
include KubernetesHelpers
it_behaves_like 'having unique enum values'
- subject { build(:cluster) }
+ subject(:cluster) { build(:cluster) }
it { is_expected.to include_module(HasEnvironmentScope) }
it { is_expected.to belong_to(:user) }
@@ -35,14 +36,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
- it { is_expected.to delegate_method(:on_creation?).to(:provider) }
- it { is_expected.to delegate_method(:knative_pre_installed?).to(:provider) }
- it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix }
- it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:integration_prometheus).with_prefix }
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
@@ -721,14 +714,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'when all applications are created' do
let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
- let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, cert_manager, prometheus, runner, jupyter, knative)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
end
end
@@ -1417,4 +1409,218 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#on_creation?' do
+ subject(:on_creation?) { cluster.on_creation? }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'without provider' do
+ let(:provider) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with provider' do
+ let(:provider) { instance_double(Clusters::Providers::Gcp, on_creation?: on_creation?) }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'with on_creation? set to true' do
+ let(:on_creation?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with on_creation? set to false' do
+ let(:on_creation?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#knative_pre_installed?' do
+ subject(:knative_pre_installed?) { cluster.knative_pre_installed? }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'without provider' do
+ let(:provider) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with provider' do
+ let(:provider) { instance_double(Clusters::Providers::Aws, knative_pre_installed?: knative_pre_installed?) }
+
+ context 'with knative_pre_installed? set to true' do
+ let(:knative_pre_installed?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with knative_pre_installed? set to false' do
+ let(:knative_pre_installed?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#platform_kubernetes_active?' do
+ subject(:platform_kubernetes_active?) { cluster.platform_kubernetes_active? }
+
+ before do
+ allow(cluster).to receive(:platform_kubernetes).and_return(platform_kubernetes)
+ end
+
+ context 'without platform_kubernetes' do
+ let(:platform_kubernetes) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with platform_kubernetes' do
+ let(:platform_kubernetes) { instance_double(Clusters::Platforms::Kubernetes, active?: active?) }
+
+ context 'with active? set to true' do
+ let(:active?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with active? set to false' do
+ let(:active?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#platform_kubernetes_rbac?' do
+ subject(:platform_kubernetes_rbac?) { cluster.platform_kubernetes_rbac? }
+
+ before do
+ allow(cluster).to receive(:platform_kubernetes).and_return(platform_kubernetes)
+ end
+
+ context 'without platform_kubernetes' do
+ let(:platform_kubernetes) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with platform_kubernetes' do
+ let(:platform_kubernetes) { instance_double(Clusters::Platforms::Kubernetes, rbac?: rbac?) }
+
+ context 'with rbac? set to true' do
+ let(:rbac?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with rbac? set to false' do
+ let(:rbac?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_helm_available?' do
+ subject(:application_helm_available?) { cluster.application_helm_available? }
+
+ before do
+ allow(cluster).to receive(:application_helm).and_return(application_helm)
+ end
+
+ context 'without application_helm' do
+ let(:application_helm) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_helm' do
+ let(:application_helm) { instance_double(Clusters::Applications::Helm, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_ingress_available?' do
+ subject(:application_ingress_available?) { cluster.application_ingress_available? }
+
+ before do
+ allow(cluster).to receive(:application_ingress).and_return(application_ingress)
+ end
+
+ context 'without application_ingress' do
+ let(:application_ingress) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_ingress' do
+ let(:application_ingress) { instance_double(Clusters::Applications::Ingress, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_knative_available?' do
+ subject(:application_knative_available?) { cluster.application_knative_available? }
+
+ before do
+ allow(cluster).to receive(:application_knative).and_return(application_knative)
+ end
+
+ context 'without application_knative' do
+ let(:application_knative) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_knative' do
+ let(:application_knative) { instance_double(Clusters::Applications::Knative, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 704203ed29c..4ff451af9de 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -802,64 +802,70 @@ RSpec.describe CommitStatus do
end
describe 'ensure stage assignment' do
- context 'when commit status has a stage_id assigned' do
- let!(:stage) do
- create(:ci_stage, project: project, pipeline: pipeline)
- end
+ before do
+ stub_feature_flags(ci_remove_ensure_stage_service: false)
+ end
- let(:commit_status) do
- create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
- end
+ context 'when the feature flag ci_remove_ensure_stage_service is disabled' do
+ context 'when commit status has a stage_id assigned' do
+ let!(:stage) do
+ create(:ci_stage, project: project, pipeline: pipeline)
+ end
- it 'does not create a new stage' do
- expect { commit_status }.not_to change { Ci::Stage.count }
- expect(commit_status.stage_id).to eq stage.id
- end
- end
+ let(:commit_status) do
+ create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
+ end
- context 'when commit status does not have a stage_id assigned' do
- let(:commit_status) do
- create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).to eq stage.id
+ end
end
- let(:stage) { Ci::Stage.first }
+ context 'when commit status does not have a stage_id assigned' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ end
- it 'creates a new stage', :sidekiq_might_not_need_inline do
- expect { commit_status }.to change { Ci::Stage.count }.by(1)
+ let(:stage) { Ci::Stage.first }
- expect(stage.name).to eq 'test'
- expect(stage.project).to eq commit_status.project
- expect(stage.pipeline).to eq commit_status.pipeline
- expect(stage.status).to eq commit_status.status
- expect(commit_status.stage_id).to eq stage.id
- end
- end
+ it 'creates a new stage', :sidekiq_might_not_need_inline do
+ expect { commit_status }.to change { Ci::Stage.count }.by(1)
- context 'when commit status does not have stage but it exists' do
- let!(:stage) do
- create(:ci_stage, project: project, pipeline: pipeline, name: 'test')
+ expect(stage.name).to eq 'test'
+ expect(stage.project).to eq commit_status.project
+ expect(stage.pipeline).to eq commit_status.pipeline
+ expect(stage.status).to eq commit_status.status
+ expect(commit_status.stage_id).to eq stage.id
+ end
end
- let(:commit_status) do
- create(:commit_status, project: project, pipeline: pipeline, name: 'rspec', stage: 'test', status: :success)
- end
+ context 'when commit status does not have stage but it exists' do
+ let!(:stage) do
+ create(:ci_stage, project: project, pipeline: pipeline, name: 'test')
+ end
- it 'uses existing stage', :sidekiq_might_not_need_inline do
- expect { commit_status }.not_to change { Ci::Stage.count }
+ let(:commit_status) do
+ create(:commit_status, project: project, pipeline: pipeline, name: 'rspec', stage: 'test', status: :success)
+ end
- expect(commit_status.stage_id).to eq stage.id
- expect(stage.reload.status).to eq commit_status.status
- end
- end
+ it 'uses existing stage', :sidekiq_might_not_need_inline do
+ expect { commit_status }.not_to change { Ci::Stage.count }
- context 'when commit status is being imported' do
- let(:commit_status) do
- create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ expect(commit_status.stage_id).to eq stage.id
+ expect(stage.reload.status).to eq commit_status.status
+ end
end
- it 'does not create a new stage' do
- expect { commit_status }.not_to change { Ci::Stage.count }
- expect(commit_status.stage_id).not_to be_present
+ context 'when commit status is being imported' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).not_to be_present
+ end
end
end
end
@@ -1007,6 +1013,10 @@ RSpec.describe CommitStatus do
describe '.stage_name' do
subject(:stage_name) { commit_status.stage_name }
+ before do
+ commit_status.ci_stage = build(:ci_stage)
+ end
+
it 'returns the stage name' do
expect(stage_name).to eq('test')
end
@@ -1023,7 +1033,7 @@ RSpec.describe CommitStatus do
describe 'partitioning' do
context 'with pipeline' do
let(:pipeline) { build(:ci_pipeline, partition_id: 123) }
- let(:status) { build(:commit_status, pipeline: pipeline) }
+ let(:status) { build(:commit_status, pipeline: pipeline, partition_id: nil) }
it 'copies the partition_id from pipeline' do
expect { status.valid? }.to change(status, :partition_id).to(123)
diff --git a/spec/models/concerns/after_commit_queue_spec.rb b/spec/models/concerns/after_commit_queue_spec.rb
index 8f091081dce..c57d388fe5d 100644
--- a/spec/models/concerns/after_commit_queue_spec.rb
+++ b/spec/models/concerns/after_commit_queue_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe AfterCommitQueue do
context 'multiple databases - Ci::ApplicationRecord models' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
table_sql = <<~SQL
CREATE TABLE _test_gitlab_ci_after_commit_queue (
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 577004c2cf6..65b7da20bbc 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkInsertSafe do
+RSpec.describe BulkInsertSafe, feature_category: :database do
before(:all) do
ActiveRecord::Schema.define do
create_table :_test_bulk_insert_parent_items, force: true do |t|
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index 9dfc7d84f89..4ef690ca4c1 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::HasStatus do
+RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.composite_status' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb
index 861d8f3b974..d7d0cabd4ae 100644
--- a/spec/models/concerns/ci/has_variable_spec.rb
+++ b/spec/models/concerns/ci/has_variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::HasVariable do
+RSpec.describe Ci::HasVariable, feature_category: :continuous_integration do
subject { build(:ci_variable) }
it { is_expected.to validate_presence_of(:key) }
@@ -113,4 +113,36 @@ RSpec.describe Ci::HasVariable do
end
end
end
+
+ describe '.order_by' do
+ let_it_be(:relation) { Ci::Variable.all }
+
+ it 'supports ordering by key ascending' do
+ expect(relation).to receive(:reorder).with({ key: :asc })
+
+ relation.order_by('key_asc')
+ end
+
+ it 'supports ordering by key descending' do
+ expect(relation).to receive(:reorder).with({ key: :desc })
+
+ relation.order_by('key_desc')
+ end
+
+ context 'when order method is unknown' do
+ it 'does not call reorder' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by('unknown')
+ end
+ end
+
+ context 'when order method is nil' do
+ it 'does not call reorder' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by(nil)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index 2b13fc21fe8..b57b2b15608 100644
--- a/spec/models/concerns/ci/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -2,15 +2,16 @@
require 'spec_helper'
-RSpec.describe Ci::Maskable do
+RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do
let(:variable) { build(:ci_variable) }
describe 'masked value validations' do
subject { variable }
- context 'when variable is masked' do
+ context 'when variable is masked and expanded' do
before do
subject.masked = true
+ subject.raw = false
end
it { is_expected.not_to allow_value('hello').for(:value) }
@@ -20,6 +21,37 @@ RSpec.describe Ci::Maskable do
it { is_expected.to allow_value('helloworld').for(:value) }
end
+ context 'when method :raw is not defined' do
+ let(:test_var_class) do
+ Struct.new(:masked?) do
+ include ActiveModel::Validations
+ include Ci::Maskable
+ end
+ end
+
+ let(:variable) { test_var_class.new(true) }
+
+ it 'evaluates masked variables as expanded' do
+ expect(subject).not_to be_masked_and_raw
+ expect(subject).to be_masked_and_expanded
+ end
+ end
+
+ context 'when variable is masked and raw' do
+ before do
+ subject.masked = true
+ subject.raw = true
+ end
+
+ it { is_expected.not_to allow_value('hello').for(:value) }
+ it { is_expected.not_to allow_value('hello world').for(:value) }
+ it { is_expected.to allow_value('hello\rworld').for(:value) }
+ it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) }
+ it { is_expected.to allow_value('helloworld!!!').for(:value) }
+ it { is_expected.to allow_value('hell******world').for(:value) }
+ it { is_expected.to allow_value('helloworld123').for(:value) }
+ end
+
context 'when variable is not masked' do
before do
subject.masked = false
@@ -33,40 +65,70 @@ RSpec.describe Ci::Maskable do
end
end
- describe 'REGEX' do
- subject { Ci::Maskable::REGEX }
+ describe 'Regexes' do
+ context 'with MASK_AND_RAW_REGEX' do
+ subject { Ci::Maskable::MASK_AND_RAW_REGEX }
- it 'does not match strings shorter than 8 letters' do
- expect(subject.match?('hello')).to eq(false)
- end
+ it 'does not match strings shorter than 8 letters' do
+ expect(subject.match?('hello')).to eq(false)
+ end
- it 'does not match strings with spaces' do
- expect(subject.match?('hello world')).to eq(false)
- end
+ it 'does not match strings with spaces' do
+ expect(subject.match?('hello world')).to eq(false)
+ end
- it 'does not match strings with shell variables' do
- expect(subject.match?('hello$VARIABLEworld')).to eq(false)
- end
+ it 'does not match strings that span more than one line' do
+ string = <<~EOS
+ hello
+ world
+ EOS
- it 'does not match strings with escape characters' do
- expect(subject.match?('hello\rworld')).to eq(false)
+ expect(subject.match?(string)).to eq(false)
+ end
+
+ it 'matches valid strings' do
+ expect(subject.match?('hello$VARIABLEworld')).to eq(true)
+ expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ expect(subject.match?('hello\rworld')).to eq(true)
+ expect(subject.match?('HelloWorld%#^')).to eq(true)
+ end
end
- it 'does not match strings that span more than one line' do
- string = <<~EOS
- hello
- world
- EOS
+ context 'with REGEX' do
+ subject { Ci::Maskable::REGEX }
- expect(subject.match?(string)).to eq(false)
- end
+ it 'does not match strings shorter than 8 letters' do
+ expect(subject.match?('hello')).to eq(false)
+ end
- it 'does not match strings using unsupported characters' do
- expect(subject.match?('HelloWorld%#^')).to eq(false)
- end
+ it 'does not match strings with spaces' do
+ expect(subject.match?('hello world')).to eq(false)
+ end
- it 'matches valid strings' do
- expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ it 'does not match strings with shell variables' do
+ expect(subject.match?('hello$VARIABLEworld')).to eq(false)
+ end
+
+ it 'does not match strings with escape characters' do
+ expect(subject.match?('hello\rworld')).to eq(false)
+ end
+
+ it 'does not match strings that span more than one line' do
+ string = <<~EOS
+ hello
+ world
+ EOS
+
+ expect(subject.match?(string)).to eq(false)
+ end
+
+ it 'does not match strings using unsupported characters' do
+ expect(subject.match?('HelloWorld%#^')).to eq(false)
+ end
+
+ it 'matches valid strings' do
+ expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ end
end
end
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
index c3831b654cf..eaebf613cb5 100644
--- a/spec/models/concerns/cross_database_modification_spec.rb
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -21,6 +21,14 @@ RSpec.describe CrossDatabaseModification do
expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ PackageMetadata::ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_pm)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
Project.transaction do
expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
diff --git a/spec/models/concerns/exportable_spec.rb b/spec/models/concerns/exportable_spec.rb
new file mode 100644
index 00000000000..74709b06403
--- /dev/null
+++ b/spec/models/concerns/exportable_spec.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Exportable, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
+ let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
+ let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
+
+ let_it_be(:model_klass) do
+ Class.new(ApplicationRecord) do
+ include Exportable
+
+ belongs_to :project
+ has_one :milestone
+ has_many :notes
+
+ self.table_name = 'issues'
+
+ def self.name
+ 'Issue'
+ end
+ end
+ end
+
+ subject { model_klass.new }
+
+ describe '.readable_records' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when model does not respond to association name' do
+ it 'returns nil' do
+ expect(subject.readable_records(:foo, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when model does respond to association name' do
+ context 'when there are no records' do
+ it 'returns nil' do
+ expect(model_record.readable_records(:notes, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when association has #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return(issue.notes)
+ end
+
+ context 'when user can read all records' do
+ before do
+ allow_next_found_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(true)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2)
+ end
+ end
+
+ context 'when user can not read records' do
+ before do
+ allow_next_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(false)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to eq([])
+ end
+ end
+ end
+
+ context 'when association does not have #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return([note1])
+
+ allow(note1).to receive(:respond_to?).and_call_original
+ allow(note1).to receive(:respond_to?).with(:exportable_record?).and_return(false)
+ end
+
+ it 'calls #readable_by?' do
+ expect(note1).to receive(:readable_by?).with(user)
+
+ model_record.readable_records(:notes, current_user: user)
+ end
+ end
+
+ context 'with single relation' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone)
+ end
+
+ context 'when user can read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone)
+ end
+ end
+
+ context 'when user can not read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ describe '.exportable_association?' do
+ context 'when model does not respond to association name' do
+ it 'returns false' do
+ expect(subject.exportable_association?(:tests)).to eq(false)
+
+ allow(issue).to receive(:respond_to?).with(:tests).and_return(false)
+ end
+ end
+
+ context 'when model responds to association name' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when association contains records' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(milestone)
+ end
+
+ context 'when current_user is not present' do
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when current_user can read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when current_user can not read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(false)
+ end
+ end
+ end
+
+ context 'when association is empty' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(nil)
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when association type is has_many' do
+ it 'returns true' do
+ expect(subject.exportable_association?(:notes)).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '.restricted_associations' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when `exportable_restricted_associations` is not defined in inheriting class' do
+ it 'returns empty array' do
+ expect(subject.restricted_associations(model_associations)).to eq([])
+ end
+ end
+
+ context 'when `exportable_restricted_associations` is defined in inheriting class' do
+ before do
+ stub_const('DummyModel', model_klass)
+
+ DummyModel.class_eval do
+ def exportable_restricted_associations
+ super + [:notes]
+ end
+ end
+ end
+
+ it 'returns empty array if provided key are not restricted' do
+ expect(subject.restricted_associations([:labels])).to eq([])
+ end
+
+ it 'returns array with restricted keys' do
+ expect(subject.restricted_associations(model_associations)).to contain_exactly(:notes)
+ end
+ end
+ end
+
+ describe '.has_many_association?' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when association type is `has_many`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:notes)).to eq(true)
+ end
+ end
+
+ context 'when association type is `has_one`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when association type is `belongs_to`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:project)).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_link_spec.rb b/spec/models/concerns/issuable_link_spec.rb
index 7be6d8a074d..aaa4de1f46b 100644
--- a/spec/models/concerns/issuable_link_spec.rb
+++ b/spec/models/concerns/issuable_link_spec.rb
@@ -40,4 +40,18 @@ RSpec.describe IssuableLink do
end
end
end
+
+ describe '.available_link_types' do
+ let(:expected_link_types) do
+ if Gitlab.ee?
+ %w[relates_to blocks is_blocked_by]
+ else
+ %w[relates_to]
+ end
+ end
+
+ subject { test_class.available_link_types }
+
+ it { is_expected.to match_array(expected_link_types) }
+ end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 383ed68816e..c1323d20d83 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Noteable do
+RSpec.describe Noteable, feature_category: :code_review_workflow do
let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
let(:project) { active_diff_note1.project }
subject { active_diff_note1.noteable }
@@ -155,31 +155,38 @@ RSpec.describe Noteable do
end
describe '#discussion_root_note_ids' do
- let!(:label_event) { create(:resource_label_event, merge_request: subject) }
+ let!(:label_event) do
+ create(:resource_label_event, merge_request: subject).tap do |event|
+ # Create an extra label event that should get grouped with the above event so this one should not
+ # be included in the resulting root nodes
+ create(:resource_label_event, merge_request: subject, user: event.user, created_at: event.created_at)
+ end
+ end
+
let!(:system_note) { create(:system_note, project: project, noteable: subject) }
let!(:milestone_event) { create(:resource_milestone_event, merge_request: subject) }
let!(:state_event) { create(:resource_state_event, merge_request: subject) }
it 'returns ordered discussion_ids and synthetic note ids' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:all_notes]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
+ a_hash_including(table_name: 'notes', id: active_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: active_diff_note3.id),
+ a_hash_including(table_name: 'notes', id: outdated_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note2.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note3.id),
+ a_hash_including(table_name: 'notes', id: note1.id),
+ a_hash_including(table_name: 'notes', id: note2.id),
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
- a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'notes', id: system_note.id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
@@ -187,34 +194,34 @@ RSpec.describe Noteable do
it 'filters by comments only' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_comments]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
+ a_hash_including(table_name: 'notes', id: active_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: active_diff_note3.id),
+ a_hash_including(table_name: 'notes', id: outdated_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note2.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note3.id),
+ a_hash_including(table_name: 'notes', id: note1.id),
+ a_hash_including(table_name: 'notes', id: note2.id)
])
end
it 'filters by system notes only' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_activity]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
- a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'notes', id: system_note.id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 87f1dc5a27b..059df64f7d0 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PgFullTextSearchable do
+RSpec.describe PgFullTextSearchable, feature_category: :global_search do
let(:project) { build(:project, project_namespace: build(:project_namespace)) }
let(:model_class) do
diff --git a/spec/models/concerns/require_email_verification_spec.rb b/spec/models/concerns/require_email_verification_spec.rb
index d087b2864f8..0a6293f852e 100644
--- a/spec/models/concerns/require_email_verification_spec.rb
+++ b/spec/models/concerns/require_email_verification_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RequireEmailVerification do
+RSpec.describe RequireEmailVerification, feature_category: :insider_threat do
let_it_be(:model) do
Class.new(ApplicationRecord) do
self.table_name = 'users'
@@ -15,11 +15,15 @@ RSpec.describe RequireEmailVerification do
using RSpec::Parameterized::TableSyntax
- where(:feature_flag_enabled, :two_factor_enabled, :overridden) do
- false | false | false
- false | true | false
- true | false | true
- true | true | false
+ where(:feature_flag_enabled, :two_factor_enabled, :skipped, :overridden) do
+ false | false | false | false
+ false | false | true | false
+ false | true | false | false
+ false | true | true | false
+ true | false | false | true
+ true | false | true | false
+ true | true | false | false
+ true | true | true | false
end
with_them do
@@ -29,6 +33,7 @@ RSpec.describe RequireEmailVerification do
before do
stub_feature_flags(require_email_verification: feature_flag_enabled ? instance : another_instance)
allow(instance).to receive(:two_factor_enabled?).and_return(two_factor_enabled)
+ stub_feature_flags(skip_require_email_verification: skipped ? instance : another_instance)
end
describe '#lock_access!' do
diff --git a/spec/models/concerns/sensitive_serializable_hash_spec.rb b/spec/models/concerns/sensitive_serializable_hash_spec.rb
index 0bfd2d6a7de..7d646106061 100644
--- a/spec/models/concerns/sensitive_serializable_hash_spec.rb
+++ b/spec/models/concerns/sensitive_serializable_hash_spec.rb
@@ -46,8 +46,8 @@ RSpec.describe SensitiveSerializableHash do
context "#{klass.name}\##{attribute_name}" do
let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
- it 'has a encrypted_attributes field' do
- expect(klass.encrypted_attributes).to include(attribute_name.to_sym)
+ it 'has a attr_encrypted_attributes field' do
+ expect(klass.attr_encrypted_attributes).to include(attribute_name.to_sym)
end
it 'does not include the attribute in serializable_hash', :aggregate_failures do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index baa2d75705a..44cf87aa1c1 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -202,5 +202,21 @@ RSpec.describe Spammable do
expect(issue.submittable_as_spam_by?(nil)).to be_nil
end
end
+
+ describe '#allow_possible_spam?' do
+ subject { issue.allow_possible_spam? }
+
+ context 'when the `allow_possible_spam` application setting is turned off' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the `allow_possible_spam` application setting is turned on' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
end
end
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
index 140f6cda51c..0ad29454ff3 100644
--- a/spec/models/concerns/taskable_spec.rb
+++ b/spec/models/concerns/taskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Taskable do
+RSpec.describe Taskable, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
describe '.get_tasks' do
@@ -13,8 +13,18 @@ RSpec.describe Taskable do
- [x] Second item
* [x] First item
* [ ] Second item
+
+ <!-- a comment
+ - [ ] Item in comment, ignore
+ rest of comment -->
+
+ [ ] No-break space (U+00A0)
+ [ ] Figure space (U+2007)
+
+ ```
+ - [ ] Item in code, ignore
+ ```
+
+ [ ] Narrow no-break space (U+202F)
+ [ ] Thin space (U+2009)
MARKDOWN
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 5682a189c41..28cda269458 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe TriggerableHooks do
TestableHook.class_eval do
include TriggerableHooks # rubocop:disable RSpec/DescribedClass
triggerable_hooks [:push_hooks]
+
+ scope :executable, -> { all }
end
end
diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb
index c2c494c49fb..07ac35f7b6a 100644
--- a/spec/models/container_registry/event_spec.rb
+++ b/spec/models/container_registry/event_spec.rb
@@ -116,6 +116,24 @@ RSpec.describe ContainerRegistry::Event do
subject { described_class.new(raw_event).track! }
+ shared_examples 'tracking event is sent to HLLRedisCounter with event and originator ID' do |originator_type|
+ it 'fetches the event originator based on username' do
+ count.times do
+ expect(User).to receive(:find_by_username).with(originator.username)
+ end
+
+ subject
+ end
+
+ it 'sends a tracking event to HLLRedisCounter' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event).with("i_container_registry_#{event}_#{originator_type}", values: originator.id)
+ .exactly(count).time
+
+ subject
+ end
+ end
+
context 'with a respository target' do
let(:target) do
{
@@ -164,5 +182,58 @@ RSpec.describe ContainerRegistry::Event do
end
end
end
+
+ context 'with a deploy token as the actor' do
+ let!(:originator) { create(:deploy_token, username: 'username', id: 3) }
+ let(:raw_event) do
+ {
+ 'action' => 'push',
+ 'target' => { 'tag' => 'latest' },
+ 'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username }
+ }
+ end
+
+ it 'does not send a tracking event to HLLRedisCounter' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ subject
+ end
+ end
+
+ context 'with a user as the actor' do
+ let_it_be(:originator) { create(:user, username: 'username') }
+ let(:raw_event) do
+ {
+ 'action' => action,
+ 'target' => target,
+ 'actor' => { 'user_type' => user_type, 'name' => originator.username }
+ }
+ end
+
+ where(:target, :action, :event, :user_type, :count) do
+ { 'tag' => 'latest' } | 'push' | 'push_tag' | 'personal_access_token' | 1
+ { 'tag' => 'latest' } | 'delete' | 'delete_tag' | 'personal_access_token' | 1
+ { 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 'build' | 1
+ { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'gitlab_or_ldap' | 1
+ { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'not_a_user' | 0
+ { 'tag' => 'latest' } | 'copy' | '' | nil | 0
+ { 'repository' => 'foo/bar' } | 'copy' | '' | '' | 0
+ end
+
+ with_them do
+ it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :user
+ end
+ end
+
+ context 'without an actor name' do
+ let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } }
+
+ it 'does not send a tracking event to HLLRedisCounter' do
+ expect(User).not_to receive(:find_by_username)
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 33d3cabb325..da7b54644bd 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRepository, :aggregate_failures do
+RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
let(:group) { create(:group, name: 'group') }
diff --git a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
index ee13aae50dc..1516de8defd 100644
--- a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
+++ b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
@@ -10,11 +10,14 @@ RSpec.describe CycleAnalytics::ProjectLevelStageAdapter, type: :model do
end
end
- let_it_be(:project) { merge_request.target_project }
+ let_it_be(:project) { merge_request.target_project.reload }
let(:stage) do
- params = Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(stage_name).merge(project: project)
- Analytics::CycleAnalytics::ProjectStage.new(params)
+ params = Gitlab::Analytics::CycleAnalytics::DefaultStages
+ .find_by_name!(stage_name)
+ .merge(namespace: project.project_namespace)
+
+ Analytics::CycleAnalytics::Stage.new(params)
end
around do |example|
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 3272d5236d3..337fa40b4ba 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe DeployKey, :mailer do
is_expected.to have_many(:deploy_keys_projects_with_write_access)
.conditions(can_push: true)
.class_name('DeployKeysProject')
+ .inverse_of(:deploy_key)
end
it do
@@ -20,7 +21,8 @@ RSpec.describe DeployKey, :mailer do
end
it { is_expected.to have_many(:projects) }
- it { is_expected.to have_many(:protected_branch_push_access_levels) }
+ it { is_expected.to have_many(:protected_branch_push_access_levels).inverse_of(:deploy_key) }
+ it { is_expected.to have_many(:protected_tag_create_access_levels).inverse_of(:deploy_key) }
end
describe 'notification' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index f0fdc62e6c7..46a1b4ce588 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployment do
+RSpec.describe Deployment, feature_category: :continuous_delivery do
subject { build(:deployment) }
it { is_expected.to belong_to(:project).required }
@@ -164,7 +164,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running',
+ 'status_changed_at' => Time.current.to_s }))
deployment.run!
end
@@ -200,8 +201,9 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'success', status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'success',
+ 'status_changed_at' => Time.current.to_s }))
deployment.succeed!
end
@@ -231,7 +233,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'failed', status_changed_at: Time.current)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'failed',
+ 'status_changed_at' => Time.current.to_s }))
deployment.drop!
end
@@ -261,8 +264,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'canceled', status_changed_at: Time.current)
-
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'canceled',
+ 'status_changed_at' => Time.current.to_s }))
deployment.cancel!
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index b0601ea3f08..57e0d1cad8b 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::Design do
+RSpec.describe DesignManagement::Design, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:issue) { create(:issue) }
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 7bd3c5743a6..1c9798c6d99 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Discussion do
+RSpec.describe Discussion, feature_category: :team_planning do
subject { described_class.new([first_note, second_note, third_note]) }
let(:first_note) { create(:diff_note_on_merge_request) }
@@ -70,4 +70,67 @@ RSpec.describe Discussion do
end
end
end
+
+ describe '#to_global_id' do
+ context 'with a single DiffNote discussion' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([first_note], merge_request)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("DiffDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with multiple DiffNotes discussion' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([first_note, second_note], merge_request)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("DiffDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with discussions on issue' do
+ let_it_be(:note_1, refind: true) { create(:note) }
+ let_it_be(:noteable) { note_1.noteable }
+
+ context 'with a single Note' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([note_1], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with multiple Notes' do
+ let_it_be(:note_1, refind: true) { create(:note, type: 'DiscussionNote') }
+ let_it_be(:note_2, refind: true) { create(:note, in_reply_to: note_1) }
+
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([note_1, note_2], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("Discussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+ end
+
+ context 'with system notes' do
+ let_it_be(:system_note, refind: true) { create(:note, system: true) }
+ let_it_be(:noteable) { system_note.noteable }
+
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([system_note], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 0d53ebdefe9..dfb7de34993 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -62,32 +62,31 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
expect(environment).not_to be_valid
end
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 'tier' do
+ let!(:env) { build(:environment, tier: nil) }
- context 'raises error for legacy approach' do
- let!(:error_pattern) { /Preloading instance dependent scopes is not supported/ }
+ before do
+ # Disable `before_validation: :ensure_environment_tier` since it always set tier and interfere with tests.
+ # See: https://github.com/thoughtbot/shoulda/issues/178#issuecomment-1654014
- subject { described_class.preload(association_name).find_by(id: environment) }
+ allow_any_instance_of(described_class).to receive(:ensure_environment_tier).and_return(env)
+ end
- shared_examples 'raises error' do
- it do
- expect { subject }.to raise_error(error_pattern)
+ context 'presence is checked' do
+ it 'during create and update' do
+ expect(env).to validate_presence_of(:tier).on(:create)
+ expect(env).to validate_presence_of(:tier).on(:update)
end
end
- associations.each do |association|
- context association.to_s do
- let!(:association_name) { association }
-
- include_examples "raises error"
+ context 'when FF is disabled' do
+ before do
+ stub_feature_flags(validate_environment_tier_presence: false)
end
+
+ it { expect(env).to validate_presence_of(:tier).on(:create) }
+ it { expect(env).not_to validate_presence_of(:tier).on(:update) }
end
end
end
@@ -145,7 +144,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
environment = create(:environment, name: 'gprd')
environment.update_column(:tier, nil)
- expect { environment.stop! }.to change { environment.reload.tier }.from(nil).to('production')
+ expect { environment.save! }.to change { environment.reload.tier }.from(nil).to('production')
end
it 'does not overwrite the existing environment tier' do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index f170eeb5841..931d12b7109 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Event, feature_category: :users do
+RSpec.describe Event, feature_category: :user_profile do
let_it_be_with_reload(:project) { create(:project) }
describe "Associations" do
diff --git a/spec/models/factories_spec.rb b/spec/models/factories_spec.rb
deleted file mode 100644
index d6e746986d6..00000000000
--- a/spec/models/factories_spec.rb
+++ /dev/null
@@ -1,211 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# `:saas` is used to test `gitlab_subscription` factory.
-# It's not available on FOSS but also this very factory is not.
-RSpec.describe 'factories', :saas, :with_license, feature_category: :tooling do
- include Database::DatabaseHelpers
-
- # Used in `skipped` and indicates whether to skip any traits including the
- # plain factory.
- any = Object.new
-
- # https://gitlab.com/groups/gitlab-org/-/epics/5464 tracks the remaining
- # skipped factories or traits.
- #
- # Consider adding a code comment if a trait cannot produce a valid object.
- skipped = [
- [:audit_event, :unauthenticated],
- [:ci_build_trace_chunk, :fog_with_data],
- [:ci_job_artifact, :remote_store],
- [:ci_job_artifact, :raw],
- [:ci_job_artifact, :gzip],
- [:ci_job_artifact, :correct_checksum],
- [:dependency_proxy_blob, :remote_store],
- [:environment, :non_playable],
- [:composer_cache_file, :object_storage],
- [:debian_project_component_file, :object_storage],
- [:debian_project_distribution, :object_storage],
- [:debian_file_metadatum, :unknown],
- [:issue_customer_relations_contact, :for_contact],
- [:issue_customer_relations_contact, :for_issue],
- [:package_file, :object_storage],
- [:rpm_repository_file, :object_storage],
- [:pages_domain, :without_certificate],
- [:pages_domain, :without_key],
- [:pages_domain, :with_missing_chain],
- [:pages_domain, :with_trusted_chain],
- [:pages_domain, :with_trusted_expired_chain],
- [:pages_domain, :explicit_ecdsa],
- [:project_member, :blocked],
- [:remote_mirror, :ssh],
- [:user_preference, :only_comments],
- [: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],
- [:ee_ci_job_artifact, :v1_1],
- [:ee_ci_job_artifact, :v2],
- [:ee_ci_job_artifact, :v2_1],
- [:geo_ci_secure_file_state, any],
- [:geo_dependency_proxy_blob_state, any],
- [:geo_event_log, :geo_event],
- [:geo_job_artifact_state, any],
- [:geo_lfs_object_state, any],
- [:geo_pages_deployment_state, any],
- [:geo_upload_state, any],
- [:geo_ci_secure_file_state, any],
- [:lfs_object, :checksum_failure],
- [:lfs_object, :checksummed],
- [:merge_request, :blocked],
- [:merge_request_diff, :verification_failed],
- [:merge_request_diff, :verification_succeeded],
- [:package_file, :verification_failed],
- [:package_file, :verification_succeeded],
- [:project, :with_vulnerabilities],
- [:scan_execution_policy, :with_schedule_and_agent],
- [:vulnerability, :with_cluster_image_scanning_finding],
- [:vulnerability, :with_findings],
- [:vulnerability_export, :finished]
- ].freeze
-
- shared_examples 'factory' do |factory|
- skip_any = skipped.include?([factory.name, any])
-
- describe "#{factory.name} factory" do
- it 'does not raise error when built' do
- # We use `skip` here because using `build` mostly work even if
- # factories break when creating them.
- skip 'Factory skipped linting due to legacy error' if skip_any
-
- expect { build(factory.name) }.not_to raise_error
- end
-
- it 'does not raise error when created' do
- pending 'Factory skipped linting due to legacy error' if skip_any
-
- expect { create(factory.name) }.not_to raise_error # rubocop:disable Rails/SaveBang
- end
-
- factory.definition.defined_traits.map(&:name).each do |trait_name|
- skip_trait = skip_any || skipped.include?([factory.name, trait_name.to_sym])
-
- describe "linting :#{trait_name} trait" do
- it 'does not raise error when created' do
- pending 'Trait skipped linting due to legacy error' if skip_trait
-
- expect { create(factory.name, trait_name) }.not_to raise_error
- end
- end
- end
- end
- end
-
- # FactoryDefault speed up specs by creating associations only once
- # and reuse them in other factories.
- #
- # However, for some factories we cannot use FactoryDefault because the
- # associations must be unique and cannot be reused, or the factory default
- # is being mutated.
- skip_factory_defaults = %i[
- ci_job_token_project_scope_link
- ci_subscriptions_project
- evidence
- exported_protected_branch
- fork_network_member
- group_member
- import_state
- issue_customer_relations_contact
- member_task
- merge_request_block
- milestone_release
- namespace
- project_namespace
- project_repository
- project_security_setting
- prometheus_alert
- prometheus_alert_event
- prometheus_metric
- protected_branch
- protected_branch_merge_access_level
- protected_branch_push_access_level
- protected_branch_unprotect_access_level
- protected_tag
- protected_tag_create_access_level
- release
- release_link
- self_managed_prometheus_alert_event
- shard
- users_star_project
- vulnerabilities_finding_identifier
- wiki_page
- wiki_page_meta
- ].to_set.freeze
-
- # Some factories and their corresponding models are based on
- # database views. In order to use those, we have to swap the
- # view out with a table of the same structure.
- factories_based_on_view = %i[
- postgres_index
- postgres_index_bloat_estimate
- postgres_autovacuum_activity
- ].to_set.freeze
-
- without_fd, with_fd = FactoryBot.factories
- .partition { |factory| skip_factory_defaults.include?(factory.name) }
-
- # Some EE models check licensed features so stub them.
- shared_context 'with licensed features' do
- licensed_features = %i[
- board_milestone_lists
- board_assignee_lists
- ].index_with(true)
-
- if Gitlab.jh?
- licensed_features.merge! %i[
- dingtalk_integration
- feishu_bot_integration
- ].index_with(true)
- end
-
- before do
- stub_licensed_features(licensed_features)
- end
- end
-
- include_context 'with licensed features' if Gitlab.ee?
-
- context 'with factory defaults', factory_default: :keep do
- let_it_be(:namespace) { create_default(:namespace).freeze }
- let_it_be(:project) { create_default(:project, :repository).freeze }
- let_it_be(:user) { create_default(:user).freeze }
-
- before do
- factories_based_on_view.each do |factory|
- view = build(factory).class.table_name
- view_gitlab_schema = Gitlab::Database::GitlabSchema.table_schema(view)
- Gitlab::Database.database_base_models.each_value.select do |base_model|
- connection = base_model.connection
- next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(view_gitlab_schema)
-
- swapout_view_for_table(view, connection: connection)
- end
- end
- end
-
- with_fd.each do |factory|
- it_behaves_like 'factory', factory
- end
- end
-
- context 'without factory defaults' do
- without_fd.each do |factory|
- it_behaves_like 'factory', factory
- end
- end
-end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4605c086763..0a05c558d45 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Group, feature_category: :subgroups do
it { is_expected.to have_many(:requesters).dependent(:destroy) }
it { is_expected.to have_many(:namespace_requesters) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_many(:namespace_members_and_requesters) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
@@ -93,6 +94,34 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe '#namespace_members_and_requesters' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:requester) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:invited_member) { create(:group_member, :invited, :owner, group: group) }
+
+ before do
+ group.request_access(requester)
+ group.add_developer(developer)
+ end
+
+ it 'includes the correct users' do
+ expect(group.namespace_members_and_requesters).to include(
+ Member.find_by(user: requester),
+ Member.find_by(user: developer),
+ Member.find(invited_member.id)
+ )
+ end
+
+ it 'is equivalent to #members_and_requesters' do
+ expect(group.namespace_members_and_requesters).to match_array group.members_and_requesters
+ end
+
+ it_behaves_like 'query without source filters' do
+ subject { group.namespace_members_and_requesters }
+ end
+ end
+
shared_examples 'polymorphic membership relationship' do
it do
expect(membership.attributes).to include(
@@ -139,6 +168,24 @@ RSpec.describe Group, feature_category: :subgroups do
it_behaves_like 'member_namespace membership relationship'
end
+ describe '#namespace_members_and_requesters setters' do
+ let(:requested_at) { Time.current }
+ let(:user) { create(:user) }
+ let(:membership) do
+ group.namespace_members_and_requesters.create!(
+ user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER
+ )
+ end
+
+ it { expect(membership).to be_instance_of(GroupMember) }
+ it { expect(membership.user).to eq user }
+ it { expect(membership.group).to eq group }
+ it { expect(membership.requested_at).to eq requested_at }
+
+ it_behaves_like 'polymorphic membership relationship'
+ it_behaves_like 'member_namespace membership relationship'
+ end
+
describe '#members & #requesters' do
let_it_be(:requester) { create(:user) }
let_it_be(:developer) { create(:user) }
@@ -422,7 +469,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
group.save!
- group.reload
end
it { expect(group.traversal_ids).to eq [group.id] }
@@ -434,7 +480,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
group.save!
- reload_models(parent, group)
end
it { expect(parent.traversal_ids).to eq [parent.id] }
@@ -449,7 +494,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
parent.update!(parent: new_grandparent)
group.save!
- reload_models(parent, group)
end
it 'avoid traversal_ids race condition' do
@@ -487,7 +531,6 @@ RSpec.describe Group, feature_category: :subgroups do
new_parent.update!(parent: new_grandparent)
group.save!
- reload_models(parent, group, new_grandparent, new_parent)
end
it 'avoids traversal_ids race condition' do
@@ -509,14 +552,13 @@ RSpec.describe Group, feature_category: :subgroups do
end
context 'within the same hierarchy' do
- let!(:root) { create(:group).reload }
+ let!(:root) { create(:group) }
let!(:old_parent) { create(:group, parent: root) }
let!(:new_parent) { create(:group, parent: root) }
context 'with FOR NO KEY UPDATE lock' do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -537,7 +579,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -567,7 +608,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -589,7 +629,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -614,11 +653,12 @@ RSpec.describe Group, feature_category: :subgroups do
before do
parent_group.update!(parent: new_grandparent)
+ reload_models(parent_group, group)
end
it 'updates traversal_ids for all descendants' do
- expect(parent_group.reload.traversal_ids).to eq [new_grandparent.id, parent_group.id]
- expect(group.reload.traversal_ids).to eq [new_grandparent.id, parent_group.id, group.id]
+ expect(parent_group.traversal_ids).to eq [new_grandparent.id, parent_group.id]
+ expect(group.traversal_ids).to eq [new_grandparent.id, parent_group.id, group.id]
end
end
end
@@ -1006,23 +1046,15 @@ RSpec.describe Group, feature_category: :subgroups do
describe '#add_user' do
let(:user) { create(:user) }
- it 'adds the user with a blocking refresh by default' do
+ it 'adds the user' do
expect_next_instance_of(GroupMember) do |member|
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: true)
+ expect(member).to receive(:refresh_member_authorized_projects).and_call_original
end
group.add_member(user, GroupMember::MAINTAINER)
expect(group.group_members.maintainers.map(&:user)).to include(user)
end
-
- it 'passes the blocking refresh value to member' do
- expect_next_instance_of(GroupMember) do |member|
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: false)
- end
-
- group.add_member(user, GroupMember::MAINTAINER, blocking_refresh: false)
- end
end
describe '#add_users' do
@@ -2913,6 +2945,22 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe "#access_level_roles" do
+ let(:group) { create(:group) }
+
+ it "returns the correct roles" do
+ expect(group.access_level_roles).to eq(
+ {
+ 'Guest' => 10,
+ 'Reporter' => 20,
+ 'Developer' => 30,
+ 'Maintainer' => 40,
+ 'Owner' => 50
+ }
+ )
+ end
+ end
+
describe '#membership_locked?' do
it 'returns false' do
expect(build(:group)).not_to be_membership_locked
@@ -3557,13 +3605,6 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
- describe '#work_items_create_from_markdown_feature_flag_enabled?' do
- it_behaves_like 'checks self and root ancestor feature flag' do
- let(:feature_flag) { :work_items_create_from_markdown }
- let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
- end
- end
-
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }
@@ -3676,4 +3717,28 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
end
+
+ describe '#readme_project' do
+ it 'returns groups project containing metadata' do
+ readme_project = create(:project, path: Group::README_PROJECT_PATH, namespace: group)
+ create(:project, namespace: group)
+
+ expect(group.readme_project).to eq(readme_project)
+ end
+ end
+
+ describe '#group_readme' do
+ it 'returns readme from group readme project' do
+ create(:project, :repository, path: Group::README_PROJECT_PATH, namespace: group)
+
+ expect(group.group_readme.name).to eq('README.md')
+ expect(group.group_readme.data).to include('testme')
+ end
+
+ it 'returns nil if no readme project is present' do
+ create(:project, :repository, namespace: group)
+
+ expect(group.group_readme).to be(nil)
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 3d8c377ab21..c3484c4a42c 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -2,7 +2,19 @@
require 'spec_helper'
-RSpec.describe ProjectHook do
+RSpec.describe ProjectHook, feature_category: :integrations do
+ include_examples 'a hook that gets automatically disabled on failure' do
+ let_it_be(:project) { create(:project) }
+
+ let(:hook) { build(:project_hook, project: project) }
+ let(:hook_factory) { :project_hook }
+ let(:default_factory_arguments) { { project: project } }
+
+ def find_hooks
+ project.hooks
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :project }
end
@@ -67,17 +79,91 @@ RSpec.describe ProjectHook do
end
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
- let(:hook) { build(:project_hook) }
+ let_it_be(:hook) { create(:project_hook) }
+
+ def last_failure
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(hook.project.last_failure_redis_key)
+ end
+ end
+
+ def any_failed?
+ Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Utils.to_boolean(redis.get(hook.project.web_hook_failure_redis_key))
+ end
+ end
it 'is a method of this class' do
expect { hook.update_last_failure }.not_to raise_error
end
context 'when the hook is executable' do
- it 'does not update the state' do
- expect(Gitlab::Redis::SharedState).not_to receive(:with)
+ let(:redis_key) { hook.project.web_hook_failure_redis_key }
+
+ def redis_value
+ any_failed?
+ end
+
+ context 'when the state was previously failing' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_key, true)
+ end
+ end
+
+ it 'does update the state' do
+ expect { hook.update_last_failure }.to change { redis_value }.to(false)
+ end
+
+ context 'when there is another failing sibling hook' do
+ before do
+ create(:project_hook, :permanently_disabled, project: hook.project)
+ end
+
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(true)
+ end
+
+ it 'caches the current value' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with(redis_key, 'true', ex: 1.hour).and_call_original
+ end
+
+ hook.update_last_failure
+ end
+ end
+ end
+
+ context 'when the state was previously unknown' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(redis_key)
+ end
+ end
+
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(nil)
+ end
+ end
+
+ context 'when the state was previously not failing' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_key, false)
+ end
+ end
- hook.update_last_failure
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(false)
+ end
+
+ it 'does not cache the current value' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ hook.update_last_failure
+ end
end
end
@@ -86,28 +172,34 @@ RSpec.describe ProjectHook do
allow(hook).to receive(:executable?).and_return(false)
end
- def last_failure
- Gitlab::Redis::SharedState.with do |redis|
- redis.get("web_hooks:last_failure:project-#{hook.project.id}")
- end
- end
-
context 'there is no prior value', :freeze_time do
- it 'updates the state' do
+ it 'updates last_failure' do
expect { hook.update_last_failure }.to change { last_failure }.to(Time.current)
end
+
+ it 'updates any_failed?' do
+ expect { hook.update_last_failure }.to change { any_failed? }.to(true)
+ end
end
- context 'there is a prior value, from before now' do
+ context 'when there is a prior last_failure, from before now' do
it 'updates the state' do
the_future = 1.minute.from_now
-
hook.update_last_failure
travel_to(the_future) do
expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601)
end
end
+
+ it 'does not change the failing state' do
+ the_future = 1.minute.from_now
+ hook.update_last_failure
+
+ travel_to(the_future) do
+ expect { hook.update_last_failure }.not_to change { any_failed? }.from(true)
+ end
+ end
end
context 'there is a prior value, from after now' do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 2ece04c7158..e52af4a32b0 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -2,7 +2,17 @@
require 'spec_helper'
-RSpec.describe ServiceHook do
+RSpec.describe ServiceHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:service_hook) }
+ let(:hook_factory) { :service_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :integration }
end
@@ -11,32 +21,6 @@ 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/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index ba94730b1dd..edb307148b6 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -2,7 +2,17 @@
require "spec_helper"
-RSpec.describe SystemHook do
+RSpec.describe SystemHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:system_hook) }
+ let(:hook_factory) { :system_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
context 'default attributes' do
let(:system_hook) { described_class.new }
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 2f0bfbd4fed..5be2b2d3bb0 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHookLog do
+RSpec.describe WebHookLog, feature_category: :integrations do
it { is_expected.to belong_to(:web_hook) }
it { is_expected.to serialize(:request_headers).as(Hash) }
@@ -94,6 +94,35 @@ RSpec.describe WebHookLog do
end
end
+ describe 'before_save' do
+ describe '#set_url_hash' do
+ let(:web_hook_log) { build(:web_hook_log, interpolated_url: interpolated_url) }
+
+ subject(:save_web_hook_log) { web_hook_log.save! }
+
+ context 'when interpolated_url is nil' do
+ let(:interpolated_url) { nil }
+
+ it { expect { save_web_hook_log }.not_to change { web_hook_log.url_hash } }
+ end
+
+ context 'when interpolated_url has a blank value' do
+ let(:interpolated_url) { ' ' }
+
+ it { expect { save_web_hook_log }.not_to change { web_hook_log.url_hash } }
+ end
+
+ context 'when interpolated_url has a value' do
+ let(:interpolated_url) { 'example@gitlab.com' }
+ let(:expected_value) { Gitlab::CryptoHelper.sha256(interpolated_url) }
+
+ it 'assigns correct digest value' do
+ expect { save_web_hook_log }.to change { web_hook_log.url_hash }.from(nil).to(expected_value)
+ end
+ end
+ end
+ end
+
describe '.delete_batch_for' do
let_it_be(:hook) { build(:project_hook) }
let_it_be(:hook2) { build(:project_hook) }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 75ff917c036..72958a54e10 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -258,7 +258,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:token, :url, :url_variables) }
end
@@ -311,88 +311,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- describe '.executable/.disabled' do
- let!(:not_executable) 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]
- ].map do |(recent_failures, disabled_until)|
- create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- let!(:executables) do
- [
- [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(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- it 'finds the correct set of project hooks' 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
- end
-
- describe '#executable?' do
- let_it_be_with_reload(:web_hook) { create(:project_hook, project: project) }
-
- where(:recent_failures, :not_until, :executable) do
- [
- [0, :not_set, true],
- [0, :past, true],
- [0, :future, true],
- [0, :now, true],
- [1, :not_set, true],
- [1, :past, true],
- [1, :future, true],
- [3, :not_set, true],
- [3, :past, true],
- [3, :future, true],
- [4, :not_set, false],
- [4, :past, true], # expired suspension
- [4, :now, false], # active suspension
- [4, :future, false] # active suspension
- ]
- end
-
- with_them do
- # Phasing means we cannot put these values in the where block,
- # which is not subject to the frozen time context.
- let(:disabled_until) do
- case not_until
- when :not_set
- nil
- when :past
- 1.minute.ago
- when :future
- 1.minute.from_now
- when :now
- Time.current
- end
- end
-
- before do
- web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
- end
-
- it 'has the correct state' do
- expect(web_hook.executable?).to eq(executable)
- end
- end
- end
-
describe '#next_backoff' do
context 'when there was no last backoff' do
before do
@@ -435,50 +353,112 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- shared_examples 'is tolerant of invalid records' do
- specify do
- hook.url = nil
+ describe '#rate_limited?' do
+ it 'is false when hook has not been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(false)
+ end
- expect(hook).to be_invalid
- run_expectation
+ expect(hook).not_to be_rate_limited
+ end
+
+ it 'is true when hook has been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(true)
+ end
+
+ expect(hook).to be_rate_limited
end
end
- describe '#enable!' do
- it 'makes a hook executable if it was marked as failed' do
- hook.recent_failures = 1000
+ describe '#rate_limit' do
+ it 'returns the hook rate limit' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:limit).and_return(10)
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ expect(hook.rate_limit).to eq(10)
end
+ end
- it 'makes a hook executable if it is currently backed off' do
- hook.recent_failures = 1000
- hook.disabled_until = 1.hour.from_now
+ describe '#to_json' do
+ it 'does not error' do
+ expect { hook.to_json }.not_to raise_error
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'does not contain binary attributes' do
+ expect(hook.to_json).not_to include('encrypted_url_variables')
end
+ end
- it 'does not update hooks unless necessary' do
- sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+ describe '#interpolated_url' do
+ subject(:hook) { build(:project_hook, project: project) }
- expect(sql_count).to eq(0)
+ context 'when the hook URL does not contain variables' do
+ before do
+ hook.url = 'http://example.com'
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: hook.url) }
+ end
+
+ it 'is not vulnerable to malicious input' do
+ hook.url = 'something%{%<foo>2147483628G}'
+ hook.url_variables = { 'foo' => '1234567890.12345678' }
+
+ expect(hook).to have_attributes(interpolated_url: hook.url)
end
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- hook.recent_failures = 1000
+ context 'when the hook URL contains variables' do
+ before do
+ hook.url = 'http://example.com/{path}/resource?token={token}'
+ hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
+
+ context 'when a variable is missing' do
+ before do
+ hook.url_variables = { 'path' => 'present' }
+ end
+
+ it 'raises an error' do
+ # We expect validations to prevent this entirely - this is not user-error
+ expect { hook.interpolated_url }
+ .to raise_error(described_class::InterpolationError, include('Missing key token'))
+ end
+ end
+
+ context 'when the URL appears to include percent formatting' do
+ before do
+ hook.url = 'http://example.com/%{path}/resource?token=%{token}'
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'succeeds, interpolates correctly' do
+ expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
+ end
end
end
end
+ describe '#update_last_failure' do
+ it 'is a method of this class' do
+ expect { described_class.new(project: project).update_last_failure }.not_to raise_error
+ end
+ end
+
+ describe '#masked_token' do
+ it { expect(hook.masked_token).to be_nil }
+
+ context 'with a token' do
+ let(:hook) { build(:project_hook, :token, project: project) }
+
+ it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
+ end
+ end
+
describe '#backoff!' do
context 'when we have not backed off before' do
- it 'does not disable the hook' do
- expect { hook.backoff! }.not_to change(hook, :executable?).from(true)
- end
-
it 'increments the recent_failures count' do
expect { hook.backoff! }.to change(hook, :recent_failures).by(1)
end
@@ -517,20 +497,6 @@ RSpec.describe WebHook, feature_category: :integrations do
expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
end
- context 'when the hook is permanently disabled' do
- before do
- allow(hook).to receive(:permanently_disabled?).and_return(true)
- end
-
- it 'does not set disabled_until' do
- expect { hook.backoff! }.not_to change(hook, :disabled_until)
- end
-
- it 'does not increment the backoff count' do
- expect { hook.backoff! }.not_to change(hook, :backoff_count)
- end
- end
-
context 'when we have backed off MAX_FAILURES times' do
before do
stub_const("#{described_class}::MAX_FAILURES", 5)
@@ -554,12 +520,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
- end
- end
end
end
@@ -585,193 +545,5 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(sql_count).to eq(0)
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.failed! }.to change(hook, :recent_failures).by(1)
- end
- end
- end
-
- describe '#disable!' do
- it 'disables a hook' do
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
- end
- end
-
- describe '#temporarily_disabled?' do
- it 'is false when not temporarily disabled' do
- expect(hook).not_to be_temporarily_disabled
- end
-
- it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
- described_class::FAILURE_THRESHOLD.times do
- hook.backoff!
- expect(hook).not_to be_temporarily_disabled
- end
-
- hook.backoff!
- expect(hook).to be_temporarily_disabled
- end
-
- context 'when hook has been told to back off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
- hook.backoff!
- end
-
- it 'is true' do
- expect(hook).to be_temporarily_disabled
- end
- end
- end
-
- describe '#permanently_disabled?' do
- it 'is false when not disabled' do
- expect(hook).not_to be_permanently_disabled
- end
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it 'is true' do
- expect(hook).to be_permanently_disabled
- end
- end
- end
-
- describe '#rate_limited?' do
- it 'is false when hook has not been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(false)
- end
-
- expect(hook).not_to be_rate_limited
- end
-
- it 'is true when hook has been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(true)
- end
-
- expect(hook).to be_rate_limited
- end
- end
-
- describe '#rate_limit' do
- it 'returns the hook rate limit' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:limit).and_return(10)
- end
-
- expect(hook.rate_limit).to eq(10)
- end
- end
-
- describe '#alert_status' do
- subject(:status) { hook.alert_status }
-
- it { is_expected.to eq :executable }
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it { is_expected.to eq :disabled }
- end
-
- context 'when hook has been backed off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD + 1)
- hook.disabled_until = 1.hour.from_now
- end
-
- it { is_expected.to eq :temporarily_disabled }
- end
- end
-
- describe '#to_json' do
- it 'does not error' do
- expect { hook.to_json }.not_to raise_error
- end
-
- it 'does not contain binary attributes' do
- expect(hook.to_json).not_to include('encrypted_url_variables')
- end
- end
-
- describe '#interpolated_url' do
- subject(:hook) { build(:project_hook, project: project) }
-
- context 'when the hook URL does not contain variables' do
- before do
- hook.url = 'http://example.com'
- end
-
- it { is_expected.to have_attributes(interpolated_url: hook.url) }
- end
-
- it 'is not vulnerable to malicious input' do
- hook.url = 'something%{%<foo>2147483628G}'
- hook.url_variables = { 'foo' => '1234567890.12345678' }
-
- expect(hook).to have_attributes(interpolated_url: hook.url)
- end
-
- context 'when the hook URL contains variables' do
- before do
- hook.url = 'http://example.com/{path}/resource?token={token}'
- hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
- end
-
- it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
-
- context 'when a variable is missing' do
- before do
- hook.url_variables = { 'path' => 'present' }
- end
-
- it 'raises an error' do
- # We expect validations to prevent this entirely - this is not user-error
- expect { hook.interpolated_url }
- .to raise_error(described_class::InterpolationError, include('Missing key token'))
- end
- end
-
- context 'when the URL appears to include percent formatting' do
- before do
- hook.url = 'http://example.com/%{path}/resource?token=%{token}'
- end
-
- it 'succeeds, interpolates correctly' do
- expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
- end
- end
- end
- end
-
- describe '#update_last_failure' do
- it 'is a method of this class' do
- expect { described_class.new.update_last_failure }.not_to raise_error
- end
- end
-
- describe '#masked_token' do
- it { expect(hook.masked_token).to be_nil }
-
- context 'with a token' do
- let(:hook) { build(:project_hook, :token, project: project) }
-
- it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
- end
end
end
diff --git a/spec/models/incident_management/timeline_event_tag_spec.rb b/spec/models/incident_management/timeline_event_tag_spec.rb
index 1ec4fa30fb5..0f2f4e5ce9f 100644
--- a/spec/models/incident_management/timeline_event_tag_spec.rb
+++ b/spec/models/incident_management/timeline_event_tag_spec.rb
@@ -36,8 +36,16 @@ RSpec.describe IncidentManagement::TimelineEventTag do
end
describe 'constants' do
- it { expect(described_class::START_TIME_TAG_NAME).to eq('Start time') }
- it { expect(described_class::END_TIME_TAG_NAME).to eq('End time') }
+ it 'contains predefined tags' do
+ expect(described_class::PREDEFINED_TAGS).to contain_exactly(
+ 'Start time',
+ 'End time',
+ 'Impact detected',
+ 'Response initiated',
+ 'Impact mitigated',
+ 'Cause identified'
+ )
+ end
end
describe '#by_names scope' do
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 9b3250e3c08..7af96c7025a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -1050,9 +1050,11 @@ RSpec.describe Integration do
expect(hash['encrypted_properties']).not_to eq(record.encrypted_properties)
expect(hash['encrypted_properties_iv']).not_to eq(record.encrypted_properties_iv)
- decrypted = described_class.decrypt(:properties,
- hash['encrypted_properties'],
- { iv: hash['encrypted_properties_iv'] })
+ decrypted = described_class.attr_decrypt(
+ :properties,
+ hash['encrypted_properties'],
+ { iv: hash['encrypted_properties_iv'] }
+ )
expect(decrypted).to eq db_props
end
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 1527ffd7278..13dd9e03ab1 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -9,13 +9,33 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
describe 'validations' do
before do
- allow(subject).to receive(:activated?).and_return(true)
+ subject.active = active
+
allow(subject).to receive(:default_channel_placeholder).and_return('placeholder')
allow(subject).to receive(:webhook_help).and_return('help')
end
- it { is_expected.to validate_presence_of :webhook }
- it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ def build_channel_list(count)
+ (1..count).map { |i| "##{i}" }.join(',')
+ end
+
+ context 'when active' do
+ let(:active) { true }
+
+ it { is_expected.to validate_presence_of :webhook }
+ it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.not_to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
+
+ context 'when inactive' do
+ let(:active) { false }
+
+ it { is_expected.not_to validate_presence_of :webhook }
+ it { is_expected.not_to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
end
describe '#execute' do
@@ -309,6 +329,10 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
context 'with multiple channel names with spaces specified' do
it_behaves_like 'with channel specified', 'slack-integration, #slack-test, @UDLP91W0A', ['slack-integration', '#slack-test', '@UDLP91W0A']
end
+
+ context 'with duplicate channel names' do
+ it_behaves_like 'with channel specified', '#slack-test,#slack-test,#slack-test-2', ['#slack-test', '#slack-test-2']
+ end
end
describe '#default_channel_placeholder' do
diff --git a/spec/models/integrations/issue_tracker_data_spec.rb b/spec/models/integrations/issue_tracker_data_spec.rb
index 233ed7b8475..285e41424c7 100644
--- a/spec/models/integrations/issue_tracker_data_spec.rb
+++ b/spec/models/integrations/issue_tracker_data_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Integrations::IssueTrackerData do
it_behaves_like Integrations::BaseDataFields
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:issues_url, :new_issue_url, :project_url) }
end
diff --git a/spec/models/integrations/jira_tracker_data_spec.rb b/spec/models/integrations/jira_tracker_data_spec.rb
index d9f91527fbb..68aa30f06ed 100644
--- a/spec/models/integrations/jira_tracker_data_spec.rb
+++ b/spec/models/integrations/jira_tracker_data_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Integrations::JiraTrackerData do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:api_url, :password, :url, :username) }
end
diff --git a/spec/models/integrations/microsoft_teams_spec.rb b/spec/models/integrations/microsoft_teams_spec.rb
index c61cc732372..4d5f4065420 100644
--- a/spec/models/integrations/microsoft_teams_spec.rb
+++ b/spec/models/integrations/microsoft_teams_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Integrations::MicrosoftTeams do
context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
- service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
+ service = Issues::CreateService.new(container: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute[:issue]
service.hook_data(issue, 'open')
end
@@ -194,7 +194,7 @@ RSpec.describe Integrations::MicrosoftTeams do
end
describe 'Pipeline events' do
- let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/models/integrations/mock_ci_spec.rb b/spec/models/integrations/mock_ci_spec.rb
index 83954812bfe..3ff47ab2f0b 100644
--- a/spec/models/integrations/mock_ci_spec.rb
+++ b/spec/models/integrations/mock_ci_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Integrations::MockCi do
describe '#commit_status' do
let(:sha) { generate(:sha) }
- def stub_request(*args)
- WebMock.stub_request(:get, integration.commit_status_path(sha)).to_return(*args)
+ def stub_request(...)
+ WebMock.stub_request(:get, integration.commit_status_path(sha)).to_return(...)
end
def commit_status
diff --git a/spec/models/integrations/zentao_tracker_data_spec.rb b/spec/models/integrations/zentao_tracker_data_spec.rb
index dca5c4d79ae..38f2fb1b3f3 100644
--- a/spec/models/integrations/zentao_tracker_data_spec.rb
+++ b/spec/models/integrations/zentao_tracker_data_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Integrations::ZentaoTrackerData do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:url, :api_url, :zentao_product_xid, :api_token) }
end
diff --git a/spec/models/issue_email_participant_spec.rb b/spec/models/issue_email_participant_spec.rb
index 09c231bbfda..8ddc9a5f478 100644
--- a/spec/models/issue_email_participant_spec.rb
+++ b/spec/models/issue_email_participant_spec.rb
@@ -7,6 +7,12 @@ RSpec.describe IssueEmailParticipant do
it { is_expected.to belong_to(:issue) }
end
+ describe 'Modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(Presentable) }
+ end
+
describe 'Validations' do
subject { build(:issue_email_participant) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fdb397932e0..e29318a7e83 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issue, feature_category: :project_management do
+RSpec.describe Issue, feature_category: :team_planning do
include ExternalAuthorizationServiceHelpers
using RSpec::Parameterized::TableSyntax
@@ -1465,16 +1465,6 @@ RSpec.describe Issue, feature_category: :project_management do
it 'only returns without_hidden issues' do
expect(described_class.without_hidden).to eq([public_issue])
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it 'returns public and hidden issues' do
- expect(described_class.without_hidden).to contain_exactly(public_issue, hidden_issue)
- end
- end
end
describe '.by_project_id_and_iid' do
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index 525690fa6b7..6cd1534c0c8 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -85,14 +85,6 @@ RSpec.describe JiraConnectInstallation, feature_category: :integrations do
let(:installation) { build(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
it { is_expected.to eq('https://gitlab.example.com') }
-
- context 'and jira_connect_oauth_self_managed feature is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it { is_expected.to eq('http://test.host') }
- end
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 92f4d6d8531..f1bc7b41cee 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -489,4 +489,12 @@ RSpec.describe Key, :mailer do
end
end
end
+
+ describe '#signing?' do
+ it 'returns whether a key can be used for signing' do
+ expect(build(:key, usage_type: :signing)).to be_signing
+ expect(build(:key, usage_type: :auth_and_signing)).to be_signing
+ expect(build(:key, usage_type: :auth)).not_to be_signing
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 4b28f619d94..6a52f12553f 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Member do
+RSpec.describe Member, feature_category: :subgroups do
include ExclusiveLeaseHelpers
using RSpec::Parameterized::TableSyntax
@@ -891,18 +891,8 @@ RSpec.describe Member do
expect(user.authorized_projects).not_to include(project)
end
- it 'successfully completes a blocking refresh', :delete do
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: true).and_call_original
-
- member.accept_invite!(user)
-
- expect(user.authorized_projects.reload).to include(project)
- end
-
- it 'successfully completes a non-blocking refresh', :delete, :sidekiq_inline do
- member.blocking_refresh = false
-
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: false).and_call_original
+ it 'successfully completes a refresh', :delete, :sidekiq_inline do
+ expect(member).to receive(:refresh_member_authorized_projects).and_call_original
member.accept_invite!(user)
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 4ac7ce95b84..c416e63b915 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -253,17 +253,13 @@ RSpec.describe GroupMember do
let(:action) { group.members.find_by(user: user).destroy! }
- it 'changes access level', :sidekiq_inline do
+ it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project_a) }.from(true).to(false)
.and change { user.can?(:guest_access, project_b) }.from(true).to(false)
.and change { user.can?(:guest_access, project_c) }.from(true).to(false)
end
- it 'schedules an AuthorizedProjectsWorker job to recalculate authorizations' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async).with([[user.id]])
-
- action
- end
+ it_behaves_like 'calls AuthorizedProjectsWorker inline to recalculate authorizations'
end
end
end
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
index b118a3c0968..4bf33eb1fce 100644
--- a/spec/models/members/member_role_spec.rb
+++ b/spec/models/members/member_role_spec.rb
@@ -78,4 +78,30 @@ RSpec.describe MemberRole, feature_category: :authentication_and_authorization d
end
end
end
+
+ describe 'callbacks' do
+ context 'for preventing deletion after member is associated' do
+ let_it_be(:member_role) { create(:member_role) }
+
+ subject(:destroy_member_role) { member_role.destroy } # rubocop: disable Rails/SaveBang
+
+ it 'allows deletion without any member associated' do
+ expect(destroy_member_role).to be_truthy
+ end
+
+ it 'prevent deletion when member is associated' do
+ create(:group_member, { group: member_role.namespace,
+ access_level: Gitlab::Access::DEVELOPER,
+ member_role: member_role })
+ member_role.members.reload
+
+ expect(destroy_member_role).to be_falsey
+ expect(member_role.errors.messages[:base])
+ .to(
+ include(s_("MemberRole|cannot be deleted because it is already assigned to a user. "\
+ "Please disassociate the member role from all users before deletion."))
+ )
+ end
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index d573fde5a74..f0069b89494 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -200,7 +200,8 @@ RSpec.describe ProjectMember do
end
it 'refreshes the authorization without calling AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:bulk_perform_and_wait)
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
project.destroy!
end
@@ -215,8 +216,9 @@ RSpec.describe ProjectMember do
expect(project.authorized_users).not_to include(user)
end
- it 'refreshes the authorization without calling UserProjectAccessChangedService' do
- expect(UserProjectAccessChangedService).not_to receive(:new)
+ it 'refreshes the authorization without calling `AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker`' do
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
user.destroy!
end
@@ -224,7 +226,8 @@ RSpec.describe ProjectMember do
context 'when importing' do
it 'does not refresh' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:bulk_perform_and_wait)
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
member = build(:project_member, project: project)
member.importing = true
@@ -250,6 +253,8 @@ RSpec.describe ProjectMember do
shared_examples_for 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
@@ -294,16 +299,11 @@ RSpec.describe ProjectMember do
project.add_member(user, Gitlab::Access::GUEST)
end
- it 'changes access level', :sidekiq_inline do
+ it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
end
- it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker to recalculate authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).to receive(:perform_async).with(project.id, user.id)
-
- action
- end
-
+ it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker inline to recalculate authorizations'
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
end
diff --git a/spec/models/merge_request/cleanup_schedule_spec.rb b/spec/models/merge_request/cleanup_schedule_spec.rb
index 1f1f33db5ed..114413e8880 100644
--- a/spec/models/merge_request/cleanup_schedule_spec.rb
+++ b/spec/models/merge_request/cleanup_schedule_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequest::CleanupSchedule do
+RSpec.describe MergeRequest::CleanupSchedule, feature_category: :code_review_workflow do
describe 'associations' do
it { is_expected.to belong_to(:merge_request) }
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 25e5e40feb7..78f9fb5b7d3 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestDiffCommit do
+RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index 7e127caa649..eee7fe67ffb 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestDiffFile do
+RSpec.describe MergeRequestDiffFile, feature_category: :code_review_workflow do
it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffFile do
let(:valid_items_for_bulk_insertion) do
build_list(:merge_request_diff_file, 10) do |mr_diff_file|
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a059d5cae9b..2e2355ba710 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -135,6 +135,15 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: []) }
let_it_be(:merge_request4) { create(:merge_request, :draft_merge_request) }
+ describe '.preload_target_project_with_namespace' do
+ subject(:mr) { described_class.preload_target_project_with_namespace.first }
+
+ it 'returns MR with the target project\'s namespace preloaded' do
+ expect(mr.association(:target_project)).to be_loaded
+ expect(mr.target_project.association(:namespace)).to be_loaded
+ end
+ end
+
describe '.review_requested' do
it 'returns MRs that have any review requests' do
expect(described_class.review_requested).to eq([merge_request1, merge_request2])
@@ -305,13 +314,41 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(subject).to be_valid
end
end
+
+ describe '#validate_target_project' do
+ let(:merge_request) do
+ build(:merge_request, source_project: project, target_project: project, importing: importing)
+ end
+
+ let(:project) { build_stubbed(:project) }
+ let(:importing) { false }
+
+ context 'when projects #merge_requests_enabled? is true' do
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+
+ context 'when projects #merge_requests_enabled? is false' do
+ let(:project) { build_stubbed(:project, merge_requests_enabled: false) }
+
+ it 'is invalid' do
+ expect(merge_request.valid?(false)).to eq false
+ expect(merge_request.errors.full_messages).to contain_exactly('Target project has disabled merge requests')
+ end
+
+ context 'when #import? is true' do
+ let(:importing) { true }
+
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+ end
+ end
end
describe 'callbacks' do
describe '#ensure_merge_request_diff' do
let(:merge_request) { build(:merge_request) }
- context 'when async_merge_request_diff_creation is true' do
+ context 'when skip_ensure_merge_request_diff is true' do
before do
merge_request.skip_ensure_merge_request_diff = true
end
@@ -323,7 +360,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
- context 'when async_merge_request_diff_creation is false' do
+ context 'when skip_ensure_merge_request_diff is false' do
before do
merge_request.skip_ensure_merge_request_diff = false
end
@@ -4566,30 +4603,12 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
describe 'transition to merged' do
- context 'when reset_merge_error_on_transition feature flag is on' do
- before do
- stub_feature_flags(reset_merge_error_on_transition: true)
- end
-
- it 'resets the merge error' do
- subject.update!(merge_error: 'temp')
+ it 'resets the merge error' do
+ subject.update!(merge_error: 'temp')
- expect { subject.mark_as_merged }.to change { subject.merge_error.present? }
- .from(true)
- .to(false)
- end
- end
-
- context 'when reset_merge_error_on_transition feature flag is off' do
- before do
- stub_feature_flags(reset_merge_error_on_transition: false)
- end
-
- it 'does not reset the merge error' do
- subject.update!(merge_error: 'temp')
-
- expect { subject.mark_as_merged }.not_to change { subject.merge_error.present? }
- end
+ expect { subject.mark_as_merged }.to change { subject.merge_error.present? }
+ .from(true)
+ .to(false)
end
end
@@ -5526,4 +5545,53 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
end
+
+ describe '#diffs_batch_cache_with_max_age?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:diffs_batch_cache_with_max_age?) { merge_request.diffs_batch_cache_with_max_age? }
+
+ it 'returns true' do
+ expect(diffs_batch_cache_with_max_age?).to be_truthy
+ end
+
+ context 'when diffs_batch_cache_with_max_age is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_cache_with_max_age: false)
+ end
+
+ it 'returns false' do
+ expect(diffs_batch_cache_with_max_age?).to be_falsey
+ end
+ end
+ end
+
+ describe '#prepared?' do
+ subject(:merge_request) { build_stubbed(:merge_request, prepared_at: prepared_at) }
+
+ context 'when prepared_at is nil' do
+ let(:prepared_at) { nil }
+
+ it 'returns false' do
+ expect(merge_request.prepared?).to be_falsey
+ end
+ end
+
+ context 'when prepared_at is not nil' do
+ let(:prepared_at) { Time.current }
+
+ it 'returns true' do
+ expect(merge_request.prepared?).to be_truthy
+ end
+ end
+ end
+
+ describe 'prepare' do
+ it 'calls NewMergeRequestWorker' do
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+ .with(subject.id, subject.author_id)
+
+ subject.prepare
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index fa8952dc0f4..374e49aea01 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Ml::Candidate, factory_default: :keep do
- let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
- let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
+RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops do
+ let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params, name: 'candidate0') }
+ let_it_be(:candidate2) do
+ create(:ml_candidates, experiment: candidate.experiment, user: create(:user), name: 'candidate2')
+ end
let_it_be(:candidate_artifact) do
FactoryBot.create(:generic_package,
@@ -109,12 +111,12 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
end
describe "#latest_metrics" do
- let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
- let!(:metric1) { create(:ml_candidate_metrics, candidate: candidate2) }
- let!(:metric2) { create(:ml_candidate_metrics, candidate: candidate2 ) }
- let!(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate2) }
+ let_it_be(:candidate3) { create(:ml_candidates, experiment: candidate.experiment) }
+ let_it_be(:metric1) { create(:ml_candidate_metrics, candidate: candidate3) }
+ let_it_be(:metric2) { create(:ml_candidate_metrics, candidate: candidate3 ) }
+ let_it_be(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate3) }
- subject { candidate2.latest_metrics }
+ subject { candidate3.latest_metrics }
it 'fetches only the last metric for the name' do
expect(subject).to match_array([metric2, metric3] )
@@ -130,4 +132,55 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
expect(subject.association_cached?(:user)).to be(true)
end
end
+
+ describe '#by_name' do
+ let(:name) { candidate.name }
+
+ subject { described_class.by_name(name) }
+
+ context 'when name matches' do
+ it 'gets the correct candidates' do
+ expect(subject).to match_array([candidate])
+ end
+ end
+
+ context 'when name matches partially' do
+ let(:name) { 'andidate' }
+
+ it 'gets the correct candidates' do
+ expect(subject).to match_array([candidate, candidate2])
+ end
+ end
+
+ context 'when name does not match' do
+ let(:name) { non_existing_record_id.to_s }
+
+ it 'does not fetch any candidate' do
+ expect(subject).to match_array([])
+ end
+ end
+ end
+
+ describe '#order_by_metric' do
+ let_it_be(:auc_metrics) do
+ create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate)
+ create(:ml_candidate_metrics, name: 'auc', value: 0.8, candidate: candidate2)
+ end
+
+ let(:direction) { 'desc' }
+
+ subject { described_class.order_by_metric('auc', direction) }
+
+ it 'orders correctly' do
+ expect(subject).to eq([candidate2, candidate])
+ end
+
+ context 'when direction is asc' do
+ let(:direction) { 'asc' }
+
+ it 'orders correctly' do
+ expect(subject).to eq([candidate, candidate2])
+ end
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 52e9f9217f5..c75331a2ab5 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -57,4 +57,21 @@ RSpec.describe Ml::Experiment do
it { is_expected.to be_empty }
end
end
+
+ describe '#with_candidate_count' do
+ let_it_be(:exp3) do
+ create(:ml_experiments, project: exp.project).tap do |e|
+ create_list(:ml_candidates, 3, experiment: e, user: nil)
+ create(:ml_candidates, experiment: exp2, user: nil)
+ end
+ end
+
+ subject { described_class.with_candidate_count.to_h { |e| [e.id, e.candidate_count] } }
+
+ it 'fetches the candidate count', :aggregate_failures do
+ expect(subject[exp.id]).to eq(0)
+ expect(subject[exp2.id]).to eq(1)
+ expect(subject[exp3.id]).to eq(3)
+ end
+ end
end
diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb
index 918ff6aa154..b0088e44087 100644
--- a/spec/models/namespace/traversal_hierarchy_spec.rb
+++ b/spec/models/namespace/traversal_hierarchy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespace::TraversalHierarchy, type: :model do
+RSpec.describe Namespace::TraversalHierarchy, type: :model, feature_category: :subgroups do
let!(:root) { create(:group, :with_hierarchy) }
describe '.for_namespace' do
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 15b80749aa2..b7cc59b5af3 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -159,6 +159,36 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
end
end
+ describe '#emails_enabled?' do
+ context 'when a group has no parent'
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent, namespace_settings: settings) }
+
+ context 'when the groups setting is changed' do
+ it 'returns false when the attribute is false' do
+ group.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when a group has a parent' do
+ it 'returns true when no parent has disabled emails' do
+ expect(group.emails_enabled?).to be_truthy
+ end
+
+ context 'when ancestor emails are disabled' do
+ it 'returns false' do
+ grandparent.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+ end
+ end
+
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index d063f4713c7..a0698ac30f5 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespace do
+RSpec.describe Namespace, feature_category: :subgroups do
include ProjectForksHelper
include ReloadHelpers
@@ -36,6 +36,8 @@ RSpec.describe Namespace do
it { is_expected.to have_many(:work_items) }
it { is_expected.to have_many :achievements }
it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') }
+ it { is_expected.to have_many(:cycle_analytics_stages) }
+ it { is_expected.to have_many(:value_streams) }
it do
is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true)
@@ -402,6 +404,62 @@ RSpec.describe Namespace do
it { is_expected.to include_module(Namespaces::Traversal::LinearScopes) }
end
+ describe '#traversal_ids' do
+ let(:namespace) { build(:group) }
+
+ context 'when namespace not persisted' do
+ it 'returns []' do
+ expect(namespace.traversal_ids).to eq []
+ end
+ end
+
+ context 'when namespace just saved' do
+ let(:namespace) { build(:group) }
+
+ before do
+ namespace.save!
+ end
+
+ it 'returns value that matches database' do
+ expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ end
+ end
+
+ context 'when namespace loaded from database' do
+ before do
+ namespace.save!
+ namespace.reload
+ end
+
+ it 'returns database value' do
+ expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ end
+ end
+
+ context 'when made a child group' do
+ let!(:namespace) { create(:group) }
+ let!(:parent_namespace) { create(:group, children: [namespace]) }
+
+ it 'returns database value' do
+ expect(namespace.traversal_ids).to eq [parent_namespace.id, namespace.id]
+ end
+ end
+
+ context 'when root_ancestor changes' do
+ let(:old_root) { create(:group) }
+ let(:namespace) { create(:group, parent: old_root) }
+ let(:new_root) { create(:group) }
+
+ it 'resets root_ancestor memo' do
+ expect(namespace.root_ancestor).to eq old_root
+
+ namespace.update!(parent: new_root)
+
+ expect(namespace.root_ancestor).to eq new_root
+ end
+ end
+ end
+
context 'traversal scopes' do
context 'recursive' do
before do
@@ -477,36 +535,60 @@ RSpec.describe Namespace do
end
context 'traversal_ids on create' do
- shared_examples 'default traversal_ids' do
- let!(:namespace) { create(:group) }
- let!(:child_namespace) { create(:group, parent: namespace) }
+ let(:parent) { create(:group) }
+ let(:child) { create(:group, parent: parent) }
- it { expect(namespace.reload.traversal_ids).to eq [namespace.id] }
- it { expect(child_namespace.reload.traversal_ids).to eq [namespace.id, child_namespace.id] }
- it { expect(namespace.sync_events.count).to eq 1 }
- it { expect(child_namespace.sync_events.count).to eq 1 }
- end
+ it { expect(parent.traversal_ids).to eq [parent.id] }
+ it { expect(child.traversal_ids).to eq [parent.id, child.id] }
+ it { expect(parent.sync_events.count).to eq 1 }
+ it { expect(child.sync_events.count).to eq 1 }
- it_behaves_like 'default traversal_ids'
+ context 'when set_traversal_ids_on_save feature flag is disabled' do
+ before do
+ stub_feature_flags(set_traversal_ids_on_save: false)
+ end
+
+ it 'only sets traversal_ids on reload' do
+ expect { parent.reload }.to change(parent, :traversal_ids).from([]).to([parent.id])
+ expect { child.reload }.to change(child, :traversal_ids).from([]).to([parent.id, child.id])
+ end
+ end
end
context 'traversal_ids on update' do
- let!(:namespace1) { create(:group) }
- let!(:namespace2) { create(:group) }
+ let(:namespace1) { create(:group) }
+ let(:namespace2) { create(:group) }
+
+ context 'when parent_id is changed' do
+ subject { namespace1.update!(parent: namespace2) }
+
+ it 'sets the traversal_ids attribute' do
+ expect { subject }.to change { namespace1.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
+ end
+
+ context 'when set_traversal_ids_on_save feature flag is disabled' do
+ before do
+ stub_feature_flags(set_traversal_ids_on_save: false)
+ end
- it 'updates the traversal_ids when the parent_id is changed' do
- expect do
- namespace1.update!(parent: namespace2)
- end.to change { namespace1.reload.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
+ it 'sets traversal_ids after reload' do
+ subject
+
+ expect { namespace1.reload }.to change(namespace1, :traversal_ids).from([]).to([namespace2.id, namespace1.id])
+ end
+ end
end
it 'creates a Namespaces::SyncEvent using triggers' do
Namespaces::SyncEvent.delete_all
- namespace1.update!(parent: namespace2)
- expect(namespace1.reload.sync_events.count).to eq(1)
+
+ expect { namespace1.update!(parent: namespace2) }.to change(namespace1.sync_events, :count).by(1)
end
it 'creates sync_events using database trigger on the table' do
+ namespace1.save!
+ namespace2.save!
+
expect { Group.update_all(traversal_ids: [-1]) }.to change(Namespaces::SyncEvent, :count).by(2)
end
@@ -1263,12 +1345,23 @@ RSpec.describe Namespace do
end
describe ".clean_path" do
- let!(:user) { create(:user, username: "johngitlab-etc") }
- let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") }
+ it "cleans the path and makes sure it's available", time_travel_to: '2023-04-20 00:07 -0700' do
+ create :user, username: "johngitlab-etc"
+ create :namespace, path: "JohnGitLab-etc1"
+ [nil, 1, 2, 3].each do |count|
+ create :namespace, path: "pickle#{count}"
+ end
- it "cleans the path and makes sure it's available" do
expect(described_class.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
expect(described_class.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
+
+ # when we have more than MAX_TRIES count of a path use a more randomized suffix
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle4")
+ create(:namespace, path: "pickle4")
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle716")
+ create(:namespace, path: "pickle716")
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle717")
+ expect(described_class.clean_path("--$--pickle@gmail.com")).to eq("pickle717")
end
end
@@ -1595,6 +1688,8 @@ RSpec.describe Namespace do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
@@ -1992,10 +2087,21 @@ RSpec.describe Namespace do
end
describe '#emails_enabled?' do
- it "is the opposite of emails_disabled" do
- group = create(:group, emails_disabled: false)
+ context 'without a persisted namespace_setting object' do
+ let(:group) { build(:group, emails_disabled: false) }
- expect(group.emails_enabled?).to be_truthy
+ it "is the opposite of emails_disabled" do
+ expect(group.emails_enabled?).to be_truthy
+ end
+ end
+
+ context 'with a persisted namespace_setting object' do
+ let(:namespace_settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:group) { build(:group, emails_disabled: false, namespace_settings: namespace_settings) }
+
+ it "is the opposite of emails_disabled" do
+ expect(group.emails_enabled?).to be_truthy
+ end
end
end
diff --git a/spec/models/namespaces/randomized_suffix_path_spec.rb b/spec/models/namespaces/randomized_suffix_path_spec.rb
new file mode 100644
index 00000000000..a2484030f3c
--- /dev/null
+++ b/spec/models/namespaces/randomized_suffix_path_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :not_owned do
+ let(:path) { 'backintime' }
+
+ subject(:suffixed_path) { described_class.new(path) }
+
+ describe '#to_s' do
+ it 'represents with given path' do
+ expect(suffixed_path.to_s).to eq('backintime')
+ end
+ end
+
+ describe '#call' do
+ it 'returns path without count when count is 0' do
+ expect(suffixed_path.call(0)).to eq('backintime')
+ end
+
+ it "returns path suffixed with count when between 0 and #{described_class::MAX_TRIES}" do
+ (1..described_class::MAX_TRIES).each do |count|
+ expect(suffixed_path.call(count)).to eq("backintime#{count}")
+ end
+ end
+
+ it 'adds a "randomized" suffix when MAX_TRIES is exhausted', time_travel_to: '1955-11-12 06:38' do
+ count = described_class::MAX_TRIES + 1
+ expect(suffixed_path.call(count)).to eq("backintime3845")
+ end
+
+ it 'adds an offset to the "randomized" suffix when MAX_TRIES is exhausted', time_travel_to: '1955-11-12 06:38' do
+ count = described_class::MAX_TRIES + 2
+ expect(suffixed_path.call(count)).to eq("backintime3846")
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 4b574540500..013070f7be5 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1482,6 +1482,7 @@ RSpec.describe Note do
end
it "expires cache for note's issue when note is destroyed" do
+ note.save!
expect_expiration(note.noteable)
note.destroy!
@@ -1643,17 +1644,6 @@ RSpec.describe Note do
match_query_count(1).for_model(DiffNotePosition))
end
end
-
- context 'when skip_notes_diff_include flag is disabled' do
- before do
- stub_feature_flags(skip_notes_diff_include: false)
- end
-
- it 'includes additional diff associations' do
- expect { subject.reload }.to match_query_count(1).for_model(NoteDiffFile).and(
- match_query_count(1).for_model(DiffNotePosition))
- end
- end
end
context 'when noteable can have diffs' do
@@ -1889,4 +1879,34 @@ RSpec.describe Note do
it { is_expected.to eq :read_internal_note }
end
end
+
+ describe '#exportable_record?' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:noteable) { create(:issue, project: project) }
+
+ subject { note.exportable_record?(user) }
+
+ context 'when not a system note' do
+ let(:note) { build(:note, noteable: noteable) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with system note' do
+ let(:note) { build(:system_note, project: project, noteable: noteable) }
+
+ it 'returns `false` when the user cannot read the note' do
+ is_expected.to be_falsey
+ end
+
+ context 'when user can read the note' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
end
diff --git a/spec/models/onboarding/learn_gitlab_spec.rb b/spec/models/onboarding/learn_gitlab_spec.rb
deleted file mode 100644
index 5e3e1f9c304..00000000000
--- a/spec/models/onboarding/learn_gitlab_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Onboarding::LearnGitlab do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:learn_gitlab_project) { create(:project, name: described_class::PROJECT_NAME) }
- let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: described_class::BOARD_NAME) }
- let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: described_class::LABEL_NAME) }
-
- before do
- learn_gitlab_project.add_developer(current_user)
- end
-
- describe '#available?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project, :board, :label, :expected_result) do
- nil | nil | nil | nil
- nil | nil | true | nil
- nil | true | nil | nil
- nil | true | true | nil
- true | nil | nil | nil
- true | nil | true | nil
- true | true | nil | nil
- true | true | true | true
- end
-
- with_them do
- before do
- allow_next_instance_of(described_class) do |learn_gitlab|
- allow(learn_gitlab).to receive(:project).and_return(project)
- allow(learn_gitlab).to receive(:board).and_return(board)
- allow(learn_gitlab).to receive(:label).and_return(label)
- end
- end
-
- subject { described_class.new(current_user).available? }
-
- it { is_expected.to be expected_result }
- end
- end
-
- describe '#project' do
- subject { described_class.new(current_user).project }
-
- it { is_expected.to eq learn_gitlab_project }
-
- context 'when it is created during trial signup' do
- let_it_be(:learn_gitlab_project) do
- create(:project, name: described_class::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial')
- end
-
- it { is_expected.to eq learn_gitlab_project }
- end
- end
-
- describe '#board' do
- subject { described_class.new(current_user).board }
-
- it { is_expected.to eq learn_gitlab_board }
- end
-
- describe '#label' do
- subject { described_class.new(current_user).label }
-
- it { is_expected.to eq learn_gitlab_label }
- end
-end
diff --git a/spec/models/packages/composer/metadatum_spec.rb b/spec/models/packages/composer/metadatum_spec.rb
index 1c888f1563c..326eba7aa0e 100644
--- a/spec/models/packages/composer/metadatum_spec.rb
+++ b/spec/models/packages/composer/metadatum_spec.rb
@@ -10,6 +10,35 @@ RSpec.describe Packages::Composer::Metadatum, type: :model do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to validate_presence_of(:target_sha) }
it { is_expected.to validate_presence_of(:composer_json) }
+
+ describe '#composer_package_type' do
+ subject { build(:composer_metadatum, package: package) }
+
+ shared_examples 'an invalid record' do
+ it do
+ expect(subject).not_to be_valid
+ expect(subject.errors.to_a).to include('Package type must be Composer')
+ end
+ end
+
+ context 'when the metadatum package_type is Composer' do
+ let(:package) { build(:composer_package) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the metadatum has no associated package' do
+ let(:package) { nil }
+
+ it_behaves_like 'an invalid record'
+ end
+
+ context 'when the metadatum package_type is not Composer' do
+ let(:package) { build(:npm_package) }
+
+ it_behaves_like 'an invalid record'
+ end
+ end
end
describe 'scopes' do
diff --git a/spec/models/packages/debian/file_entry_spec.rb b/spec/models/packages/debian/file_entry_spec.rb
index ed6372f2873..e981adf69bc 100644
--- a/spec/models/packages/debian/file_entry_spec.rb
+++ b/spec/models/packages/debian/file_entry_spec.rb
@@ -31,13 +31,6 @@ RSpec.describe Packages::Debian::FileEntry, type: :model do
describe 'validations' do
it { is_expected.to be_valid }
- context 'with FIPS mode', :fips_mode do
- it 'raises an error' do
- expect { subject.validate! }
- .to raise_error(::Packages::FIPS::DisabledError, 'Debian registry is not FIPS compliant')
- end
- end
-
describe '#filename' do
it { is_expected.to validate_presence_of(:filename) }
it { is_expected.not_to allow_value('Hé').for(:filename) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index a8bcda1242f..992cc5c4354 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -33,6 +33,26 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
it { is_expected.to contain_exactly(publication.package) }
end
+ describe '.with_debian_codename_or_suite' do
+ let_it_be(:distribution1) { create(:debian_project_distribution, :with_suite) }
+ let_it_be(:distribution2) { create(:debian_project_distribution, :with_suite) }
+
+ let_it_be(:package1) { create(:debian_package, published_in: distribution1) }
+ let_it_be(:package2) { create(:debian_package, published_in: distribution2) }
+
+ context 'with a codename' do
+ subject { described_class.with_debian_codename_or_suite(distribution1.codename).to_a }
+
+ it { is_expected.to contain_exactly(package1) }
+ end
+
+ context 'with a suite' do
+ subject { described_class.with_debian_codename_or_suite(distribution2.suite).to_a }
+
+ it { is_expected.to contain_exactly(package2) }
+ end
+ end
+
describe '.with_composer_target' do
let!(:package1) { create(:composer_package, :with_metadatum, sha: '123') }
let!(:package2) { create(:composer_package, :with_metadatum, sha: '123') }
@@ -1048,14 +1068,16 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:maven_package, project: project) }
let_it_be(:package2) { create(:maven_package, project: project) }
- let_it_be(:package3) { create(:maven_package, project: project, name: 'foo') }
+ let_it_be(:package3) { create(:maven_package, :error, project: project) }
+ let_it_be(:package4) { create(:maven_package, project: project, name: 'foo') }
+ let_it_be(:pending_destruction_package) { create(:maven_package, :pending_destruction, project: project) }
it 'returns other package versions of the same package name belonging to the project' do
- expect(package.versions).to contain_exactly(package2)
+ expect(package.versions).to contain_exactly(package2, package3)
end
it 'does not return different packages' do
- expect(package.versions).not_to include(package3)
+ expect(package.versions).not_to include(package4)
end
end
diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb
index 842ba7ad518..bc03c34f56b 100644
--- a/spec/models/packages/tag_spec.rb
+++ b/spec/models/packages/tag_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Tag, type: :model do
+RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry do
let!(:project) { create(:project) }
let!(:package) { create(:npm_package, version: '1.0.2', project: project, updated_at: 3.days.ago) }
@@ -16,14 +16,14 @@ RSpec.describe Packages::Tag, type: :model do
it { is_expected.to validate_presence_of(:name) }
end
- describe '.for_packages' do
+ describe '.for_package_ids' do
let(:package2) { create(:package, project: project, updated_at: 2.days.ago) }
let(:package3) { create(:package, project: project, updated_at: 1.day.ago) }
let!(:tag1) { create(:packages_tag, package: package) }
let!(:tag2) { create(:packages_tag, package: package2) }
let!(:tag3) { create(:packages_tag, package: package3) }
- subject { described_class.for_packages(project.packages) }
+ subject { described_class.for_package_ids(project.packages) }
it { is_expected.to match_array([tag1, tag2, tag3]) }
@@ -34,6 +34,12 @@ RSpec.describe Packages::Tag, type: :model do
it { is_expected.to match_array([tag2, tag3]) }
end
+
+ context 'with package ids' do
+ subject { described_class.for_package_ids(project.packages.select(:id)) }
+
+ it { is_expected.to match_array([tag1, tag2, tag3]) }
+ end
end
describe '.with_name' do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index f65b5ff824b..2320ff669d0 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -216,6 +216,18 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
expect(personal_access_token).to be_valid
end
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
+ it "allows creating a token with `admin_mode` scope" do
+ personal_access_token.scopes = [:api, :admin_mode]
+
+ expect(personal_access_token).to be_valid
+ end
+ end
+
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
@@ -353,19 +365,43 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
describe '`admin_mode scope' do
subject { create(:personal_access_token, user: user, scopes: ['api']) }
- context 'with administrator user' do
- let_it_be(:user) { create(:user, :admin) }
+ context 'with feature flag enabled' do
+ context 'with administrator user' do
+ let_it_be(:user) { create(:user, :admin) }
- it 'adds `admin_mode` scope before created' do
- expect(subject.scopes).to contain_exactly('api', 'admin_mode')
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
+ end
+
+ context 'with normal user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
end
end
- context 'with normal user' do
- let_it_be(:user) { create(:user) }
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
+ context 'with administrator user' do
+ let_it_be(:user) { create(:user, :admin) }
- it 'does not add `admin_mode` scope before created' do
- expect(subject.scopes).to contain_exactly('api')
+ it 'adds `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api', 'admin_mode')
+ end
+ end
+
+ context 'with normal user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 5e2aaa8b456..7d04817b621 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -55,6 +55,43 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
let(:expected_query_count) { 0 }
end
+
+ context 'for groups arising from group shares' do
+ let_it_be(:group4) { create(:group, :private) }
+ let_it_be(:group4_subgroup) { create(:group, :private, parent: group4) }
+
+ let(:groups) { [group4, group4_subgroup] }
+
+ before do
+ create(:group_group_link, :guest, shared_with_group: group1, shared_group: group4)
+ end
+
+ context 'when `include_memberships_from_group_shares_in_preloader` feature flag is disabled' do
+ before do
+ stub_feature_flags(include_memberships_from_group_shares_in_preloader: false)
+ end
+
+ it 'sets access_level to `NO_ACCESS` in cache for groups arising from group shares' do
+ described_class.new(groups, user).execute
+
+ groups.each do |group|
+ cached_access_level = group.max_member_access_for_user(user)
+
+ expect(cached_access_level).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+ end
+
+ it 'sets the right access level in cache for groups arising from group shares' do
+ described_class.new(groups, user).execute
+
+ groups.each do |group|
+ cached_access_level = group.max_member_access_for_user(user)
+
+ expect(cached_access_level).to eq(Gitlab::Access::GUEST)
+ end
+ end
+ end
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
index 1cfeeac49cd..de10653d87e 100644
--- a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
end
end
- shared_examples '#execute' do
+ describe '#execute', :request_store do
let(:projects_arg) { projects }
context 'when user is present' do
@@ -70,16 +70,4 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
end
end
end
-
- describe '#execute', :request_store do
- include_examples '#execute'
-
- context 'when projects_preloader_fix is disabled' do
- before do
- stub_feature_flags(projects_preloader_fix: false)
- end
-
- include_examples '#execute'
- end
- end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index df89e97a41f..dc4922d8114 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -94,11 +94,13 @@ RSpec.describe ProjectAuthorization do
end
end
- shared_examples_for 'logs the detail' do
+ shared_examples_for 'logs the detail' do |batch_size:|
it 'logs the detail' do
expect(Gitlab::AppLogger).to receive(:info).with(
entire_size: 3,
- message: 'Project authorizations refresh performed with delay'
+ message: 'Project authorizations refresh performed with delay',
+ total_delay: (3 / batch_size.to_f).ceil * ProjectAuthorization::SLEEP_DELAY,
+ **Gitlab::ApplicationContext.current
)
execute
@@ -124,7 +126,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
shared_examples_for 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do
@@ -149,7 +150,7 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
@@ -190,7 +191,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
before_all do
@@ -221,7 +221,7 @@ RSpec.describe ProjectAuthorization do
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
@@ -262,7 +262,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
before_all do
@@ -293,7 +292,7 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 5a32e103e0f..2c490c33747 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,6 +27,24 @@ RSpec.describe ProjectCiCdSetting do
end
end
+ describe '#set_default_for_inbound_job_token_scope_enabled' do
+ context 'when feature flag ci_inbound_job_token_scope is enabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: true)
+ end
+
+ it { is_expected.to be_inbound_job_token_scope_enabled }
+ end
+
+ context 'when feature flag ci_inbound_job_token_scope is disabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: false)
+ end
+
+ it { is_expected.not_to be_inbound_job_token_scope_enabled }
+ end
+ end
+
describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index fb6aaffdf22..fe0b46c3117 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectFeature do
+RSpec.describe ProjectFeature, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
@@ -10,6 +10,28 @@ RSpec.describe ProjectFeature do
it { is_expected.to belong_to(:project) }
+ describe 'default values' do
+ subject { Project.new.project_feature }
+
+ specify { expect(subject.builds_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.issues_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.forking_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.merge_requests_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.snippets_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.wiki_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.repository_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.metrics_dashboard_access_level).to eq(ProjectFeature::PRIVATE) }
+ specify { expect(subject.operations_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) }
+ specify { expect(subject.monitor_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.infrastructure_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.feature_flags_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.environments_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.releases_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.package_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.container_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ end
+
describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do
it 'has higher level than that of PRIVATE_FEATURES_MIN_ACCESS_LEVEL' do
described_class::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT.each do |feature, level|
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index e5232026c39..7ceb4931c4f 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -14,6 +14,30 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
+
+ describe 'checksums attribute' do
+ let(:import_state) { build(:import_state, checksums: checksums) }
+
+ before do
+ import_state.validate
+ end
+
+ context 'when the checksums attribute has invalid fields' do
+ let(:checksums) { { fetched: { issue: :foo, note: 20 } } }
+
+ it 'adds errors' do
+ expect(import_state.errors.details.keys).to include(:checksums)
+ end
+ end
+
+ context 'when the checksums attribute has valid fields' do
+ let(:checksums) { { fetched: { issue: 8, note: 2 }, imported: { issue: 3, note: 2 } } }
+
+ it 'does not add errors' do
+ expect(import_state.errors.details.keys).not_to include(:checksums)
+ end
+ end
+ end
end
describe 'Project import job' do
@@ -199,6 +223,37 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
.from(import_data).to(nil)
end
end
+
+ context 'state transition: started: [:finished, :canceled, :failed]' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project) }
+
+ where(
+ :import_type,
+ :import_status,
+ :transition,
+ :expected_checksums
+ ) do
+ 'github' | :started | :finish | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :started | :cancel | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :started | :fail_op | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :scheduled | :cancel | {}
+ 'gitlab_project' | :started | :cancel | {}
+ end
+
+ with_them do
+ before do
+ create(:import_state, status: import_status, import_type: import_type, project: project)
+ end
+
+ it 'updates (or does not update) checksums' do
+ project.import_state.send(transition)
+
+ expect(project.import_state.checksums).to eq(expected_checksums)
+ end
+ end
+ end
end
describe 'clearing `jid` after finish', :clean_gitlab_redis_cache do
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 94a2e2fe3f9..feb5985818b 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -96,6 +96,56 @@ RSpec.describe ProjectSetting, type: :model do
end
end
+ describe '#emails_enabled?' do
+ context "when a project does not have a parent group" do
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:project) { create(:project, project_setting: project_settings) }
+
+ it "returns true" do
+ expect(project.emails_enabled?).to be_truthy
+ end
+
+ it "returns false when updating project settings" do
+ project.update_attribute(:emails_disabled, false)
+ expect(project.emails_enabled?).to be_truthy
+ end
+ end
+
+ context "when a project has a parent group" do
+ let(:namespace_settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:group) { create(:group, namespace_settings: namespace_settings) }
+ let(:project) do
+ create(:project, namespace_id: group.id,
+ project_setting: project_settings)
+ end
+
+ context 'when emails have been disabled in parent group' do
+ it 'returns false' do
+ group.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when emails are enabled in parent group' do
+ before do
+ allow(project.namespace).to receive(:emails_enabled?).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(project.emails_enabled?).to be_truthy
+ end
+
+ it 'returns false when disabled at the project' do
+ project.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_enabled?).to be_falsey
+ end
+ end
+ end
+ end
+
context 'when a parent group has a parent group' do
let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4ed85844a53..dfc8919e19d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -112,6 +112,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_many(:namespace_members_and_requesters) }
it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:kubernetes_namespaces) }
@@ -121,8 +122,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
- it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) }
- it { is_expected.to have_many(:value_streams).inverse_of(:project) }
it { is_expected.to have_many(:external_pull_requests) }
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) }
@@ -404,6 +403,34 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#namespace_members_and_requesters' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:requester) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:invited_member) { create(:project_member, :invited, :owner, project: project) }
+
+ before_all do
+ project.request_access(requester)
+ project.add_developer(developer)
+ end
+
+ it 'includes the correct users' do
+ expect(project.namespace_members_and_requesters).to include(
+ Member.find_by(user: requester),
+ Member.find_by(user: developer),
+ Member.find(invited_member.id)
+ )
+ end
+
+ it 'is equivalent to #project_members' do
+ expect(project.namespace_members_and_requesters).to match_array(project.members_and_requesters)
+ end
+
+ it_behaves_like 'query without source filters' do
+ subject { project.namespace_members_and_requesters }
+ end
+ end
+
shared_examples 'polymorphic membership relationship' do
it do
expect(membership.attributes).to include(
@@ -452,6 +479,25 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it_behaves_like 'member_namespace membership relationship'
end
+ describe '#namespace_members_and_requesters setters' do
+ let_it_be(:requested_at) { Time.current }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:membership) do
+ project.namespace_members_and_requesters.create!(
+ user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER
+ )
+ end
+
+ it { expect(membership).to be_instance_of(ProjectMember) }
+ it { expect(membership.user).to eq user }
+ it { expect(membership.project).to eq project }
+ it { expect(membership.requested_at).to eq requested_at }
+
+ it_behaves_like 'polymorphic membership relationship'
+ it_behaves_like 'member_namespace membership relationship'
+ end
+
describe '#members & #requesters' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:requester) { create(:user) }
@@ -920,6 +966,29 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#invalidate_personal_projects_count_of_owner' do
+ context 'for personal projects' do
+ let_it_be(:namespace_user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: namespace_user.namespace) }
+
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(Rails.cache).to receive(:delete).with(['users', namespace_user.id, 'personal_projects_count'])
+
+ project.invalidate_personal_projects_count_of_owner
+ end
+ end
+
+ context 'for projects in groups' do
+ let_it_be(:project) { create(:project, namespace: create(:group)) }
+
+ it 'does not invalidates any cache' do
+ expect(Rails.cache).not_to receive(:delete)
+
+ project.invalidate_personal_projects_count_of_owner
+ end
+ end
+ end
+
describe '#default_pipeline_lock' do
let(:project) { build_stubbed(:project) }
@@ -2320,7 +2389,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'shared runners' do
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner, :project, :online, projects: [project]) }
+ let(:project_runner) { create(:ci_runner, :project, :online, projects: [project]) }
let(:shared_runner) { create(:ci_runner, :instance, :online) }
let(:offline_runner) { create(:ci_runner, :instance) }
@@ -2331,8 +2400,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
is_expected.to be_falsey
end
- it 'has a specific runner' do
- specific_runner
+ it 'has a project runner' do
+ project_runner
is_expected.to be_truthy
end
@@ -2343,14 +2412,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
is_expected.to be_falsey
end
- it 'checks the presence of specific runner' do
- specific_runner
+ it 'checks the presence of project runner' do
+ project_runner
- expect(project.any_online_runners? { |runner| runner == specific_runner }).to be_truthy
+ expect(project.any_online_runners? { |runner| runner == project_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
- specific_runner
+ project_runner
expect(project.any_online_runners? { false }).to be_falsey
end
@@ -3418,6 +3487,31 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#import_checksums' do
+ context 'with import_checksums' do
+ it 'returns the right checksums' do
+ project = create(:project)
+ create(:import_state, project: project, checksums: {
+ 'fetched' => {},
+ 'imported' => {}
+ })
+
+ expect(project.import_checksums).to eq(
+ 'fetched' => {},
+ 'imported' => {}
+ )
+ end
+ end
+
+ context 'without import_state' do
+ it 'returns empty hash' do
+ project = create(:project)
+
+ expect(project.import_checksums).to eq({})
+ end
+ end
+ end
+
describe '#jira_import_status' do
let_it_be(:project) { create(:project, import_type: 'jira') }
@@ -3852,10 +3946,21 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#emails_enabled?' do
- let(:project) { build(:project, emails_disabled: false) }
+ context 'without a persisted project_setting object' do
+ let(:project) { build(:project, emails_disabled: false) }
- it "is the opposite of emails_disabled" do
- expect(project.emails_enabled?).to be_truthy
+ it "is the opposite of emails_disabled" do
+ expect(project.emails_enabled?).to be_truthy
+ end
+ end
+
+ context 'with a persisted project_setting object' do
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:project) { build(:project, emails_disabled: false, project_setting: project_settings) }
+
+ it "is the opposite of emails_disabled" do
+ expect(project.emails_enabled?).to be_truthy
+ end
end
end
@@ -4693,7 +4798,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
context 'with deploy token users' do
- let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:private_project) { create(:project, :private, description: 'Match') }
+ let_it_be(:private_project2) { create(:project, :private, description: 'Match') }
+ let_it_be(:private_project3) { create(:project, :private, description: 'Mismatch') }
subject { described_class.all.public_or_visible_to_user(user) }
@@ -4703,10 +4810,16 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to eq [] }
end
- context 'deploy token user with project' do
- let_it_be(:user) { create(:deploy_token, projects: [private_project]) }
+ context 'deploy token user with projects' do
+ let_it_be(:user) { create(:deploy_token, projects: [private_project, private_project2, private_project3]) }
+
+ it { is_expected.to contain_exactly(private_project, private_project2, private_project3) }
+
+ context 'with chained filter' do
+ subject { described_class.where(description: 'Match').public_or_visible_to_user(user) }
- it { is_expected.to include(private_project) }
+ it { is_expected.to contain_exactly(private_project, private_project2) }
+ end
end
end
end
@@ -5933,6 +6046,18 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
project.execute_hooks(data, :push_hooks)
end
+ it 'executes hooks which were backed off and are no longer backed off' do
+ project = create(:project)
+ hook = create(:project_hook, project: project, push_events: true)
+ WebHook::FAILURE_THRESHOLD.succ.times { hook.backoff! }
+
+ expect_any_instance_of(ProjectHook).to receive(:async_execute).once
+
+ travel_to(hook.disabled_until + 1.second) do
+ project.execute_hooks(data, :push_hooks)
+ end
+ end
+
it 'executes the system hooks with the specified scope' do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(data, :merge_request_hooks)
@@ -7225,6 +7350,54 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#group_protected_branches' do
+ subject { project.group_protected_branches }
+
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group) }
+ let(:protected_branch) { create(:protected_branch, group: group, project: nil) }
+
+ it 'returns protected branches of the group' do
+ is_expected.to match_array([protected_branch])
+ end
+
+ context 'when project belongs to namespace' do
+ let(:project) { create(:project) }
+
+ it 'returns empty relation' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#all_protected_branches' do
+ let(:group) { create(:group) }
+ let!(:group_protected_branch) { create(:protected_branch, group: group, project: nil) }
+ let!(:project_protected_branch) { create(:protected_branch, project: subject) }
+
+ subject { create(:project, group: group) }
+
+ context 'when feature flag `group_protected_branches` enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'return all protected branches' do
+ expect(subject.all_protected_branches).to match_array([group_protected_branch, project_protected_branch])
+ end
+ end
+
+ context 'when feature flag `group_protected_branches` disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'return only project-level protected branches' do
+ expect(subject.all_protected_branches).to match_array([project_protected_branch])
+ end
+ end
+ end
+
describe '#lfs_objects_oids' do
let(:project) { create(:project) }
let(:lfs_object) { create(:lfs_object) }
@@ -8366,16 +8539,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#work_items_create_from_markdown_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_create_from_markdown_feature_flag_enabled? }
- let(:feature_flag) { :work_items_create_from_markdown }
- let(:subject_project) { group_project }
- end
- end
-
describe 'serialization' do
let(:object) { build(:project) }
@@ -8405,14 +8568,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(record_projects_target_platforms: false)
- end
-
- it_behaves_like 'does not enqueue a Projects::RecordTargetPlatformsWorker'
- end
-
context 'when not in gitlab.com' do
let(:com) { false }
@@ -8669,6 +8824,24 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '.is_importing' do
+ it 'returns projects that have import in progress' do
+ project_1 = create(:project, :import_scheduled, import_type: 'github')
+ project_2 = create(:project, :import_started, import_type: 'github')
+ create(:project, :import_finished, import_type: 'github')
+
+ expect(described_class.is_importing).to match_array([project_1, project_2])
+ end
+ end
+
+ it_behaves_like 'something that has web-hooks' do
+ let_it_be_with_reload(:object) { create(:project) }
+
+ def create_hook
+ create(:project_hook, project: object)
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 1fab07c1452..f4cf3130aa9 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe ProjectTeam do
+RSpec.describe ProjectTeam, feature_category: :subgroups do
include ProjectForksHelper
let(:maintainer) { create(:user) }
diff --git a/spec/models/projects/data_transfer_spec.rb b/spec/models/projects/data_transfer_spec.rb
new file mode 100644
index 00000000000..6d3ddbdd74e
--- /dev/null
+++ b/spec/models/projects/data_transfer_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::DataTransfer, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project) }
+
+ it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:namespace) }
+ end
+
+ describe 'scopes' do
+ describe '.current_month' do
+ subject { described_class.current_month }
+
+ it 'returns data transfer for the current month' do
+ travel_to(Time.utc(2022, 5, 2)) do
+ _past_month = create(:project_data_transfer, project: project, date: '2022-04-01')
+ current_month = create(:project_data_transfer, project: project, date: '2022-05-01')
+
+ is_expected.to match_array([current_month])
+ end
+ end
+ end
+ end
+
+ describe '.beginning_of_month' do
+ subject { described_class.beginning_of_month(time) }
+
+ let(:time) { Time.utc(2022, 5, 2) }
+
+ it { is_expected.to eq(Time.utc(2022, 5, 1)) }
+ end
+
+ describe 'unique index' do
+ before do
+ create(:project_data_transfer, project: project, date: '2022-05-01')
+ end
+
+ it 'raises unique index violation' do
+ expect { create(:project_data_transfer, project: project, namespace: project.root_namespace, date: '2022-05-01') }
+ .to raise_error(ActiveRecord::RecordNotUnique)
+ end
+
+ context 'when project was moved from one namespace to another' do
+ it 'creates a new record' do
+ expect { create(:project_data_transfer, project: project, namespace: create(:namespace), date: '2022-05-01') }
+ .to change { described_class.count }.by(1)
+ end
+ end
+
+ context 'when a different project is created' do
+ it 'creates a new record' do
+ expect { create(:project_data_transfer, project: build(:project), date: '2022-05-01') }
+ .to change { described_class.count }.by(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index b623d534f29..71e22f848cc 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -214,7 +214,6 @@ RSpec.describe ProtectedBranch do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "“jawn”") }
- let(:use_new_cache_implementation) { true }
let(:rely_on_new_cache) { true }
shared_examples_for 'hash based cache implementation' do
@@ -230,7 +229,6 @@ RSpec.describe ProtectedBranch do
end
before do
- stub_feature_flags(hash_based_cache_for_protected_branches: use_new_cache_implementation)
stub_feature_flags(rely_on_protected_branches_cache: rely_on_new_cache)
allow(described_class).to receive(:matching).and_call_original
@@ -296,48 +294,6 @@ RSpec.describe ProtectedBranch do
expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
end
-
- context 'when feature flag hash_based_cache_for_protected_branches is off' do
- let(:use_new_cache_implementation) { false }
-
- it 'does not call hash based cache implementation' do
- expect(ProtectedBranches::CacheService).not_to receive(:new)
- expect(Rails.cache).to receive(:fetch).and_call_original
-
- described_class.protected?(project, 'missing-branch')
- end
-
- it 'correctly invalidates a cache' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).once.and_call_original
-
- create(:protected_branch, project: project, name: "bar")
- # the cache is invalidated because the project has been "updated"
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
-
- it 'sets expires_in of 1 hour for the Rails cache key' do
- cache_key = described_class.protected_ref_cache_key(project, protected_branch.name)
-
- expect(Rails.cache).to receive(:fetch).with(cache_key, expires_in: 1.hour)
-
- described_class.protected?(project, protected_branch.name)
- end
-
- context 'when project is updated' do
- it 'invalidates Rails cache' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).once.and_call_original
-
- project.touch
-
- described_class.protected?(project, protected_branch.name)
- end
- end
-
- it 'correctly uses the cached version' do
- expect(described_class).not_to receive(:matching)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
- end
end
end
@@ -385,23 +341,61 @@ RSpec.describe ProtectedBranch do
end
describe "#allow_force_push?" do
- context "when the attr allow_force_push is true" do
- let(:subject_branch) { create(:protected_branch, allow_force_push: true, name: "foo") }
+ context "when feature flag disabled" do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ let(:subject_branch) { create(:protected_branch, allow_force_push: allow_force_push, name: "foo") }
+ let(:project) { subject_branch.project }
+
+ context "when the attr allow_force_push is true" do
+ let(:allow_force_push) { true }
- it "returns true" do
- project = subject_branch.project
+ it "returns true" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(true)
+ end
+ end
- expect(described_class.allow_force_push?(project, "foo")).to eq(true)
+ context "when the attr allow_force_push is false" do
+ let(:allow_force_push) { false }
+
+ it "returns false" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(false)
+ end
end
end
- context "when the attr allow_force_push is false" do
- let(:subject_branch) { create(:protected_branch, allow_force_push: false, name: "foo") }
+ context "when feature flag enabled" do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
- it "returns false" do
- project = subject_branch.project
+ where(:group_level_value, :project_level_value, :result) do
+ true | false | true
+ false | true | false
+ true | nil | true
+ false | nil | false
+ nil | nil | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+
+ unless group_level_value.nil?
+ create(:protected_branch, allow_force_push: group_level_value, name: "foo", project: nil, group: group)
+ end
+
+ unless project_level_value.nil?
+ create(:protected_branch, allow_force_push: project_level_value, name: "foo", project: project)
+ end
+ end
- expect(described_class.allow_force_push?(project, "foo")).to eq(false)
+ it "returns result" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(result)
+ end
end
end
end
@@ -434,6 +428,36 @@ RSpec.describe ProtectedBranch do
end
end
+ describe '.protected_refs' do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.protected_refs(project) }
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'call `all_protected_branches`' do
+ expect(project).to receive(:all_protected_branches)
+
+ subject
+ end
+ end
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'call `protected_branches`' do
+ expect(project).to receive(:protected_branches)
+
+ subject
+ end
+ end
+ end
+
describe '.by_name' do
let!(:protected_branch) { create(:protected_branch, name: 'master') }
let!(:another_protected_branch) { create(:protected_branch, name: 'stable') }
@@ -502,4 +526,22 @@ RSpec.describe ProtectedBranch do
it { is_expected.not_to be_default_branch }
end
end
+
+ describe '#group_level?' do
+ context 'when entity is a Group' do
+ before do
+ subject.assign_attributes(project: nil, group: build(:group))
+ end
+
+ it { is_expected.to be_group_level }
+ end
+
+ context 'when entity is a Project' do
+ before do
+ subject.assign_attributes(project: build(:project), group: nil)
+ end
+
+ it { is_expected.not_to be_group_level }
+ end
+ end
end
diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb
new file mode 100644
index 00000000000..566f8695388
--- /dev/null
+++ b/spec/models/protected_tag/create_access_level_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do
+ describe 'associations' do
+ it { is_expected.to belong_to(:deploy_key) }
+ end
+
+ describe 'validations', :aggregate_failures do
+ let_it_be(:protected_tag) { create(:protected_tag) }
+
+ it 'verifies access levels' do
+ is_expected.to validate_inclusion_of(:access_level).in_array(
+ [
+ Gitlab::Access::MAINTAINER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ]
+ )
+ end
+
+ context 'when deploy key enabled for the project' do
+ let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
+
+ it 'is valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+
+ expect(level).to be_valid
+ end
+ end
+
+ context 'when a record exists with the same access level' do
+ before do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag)
+ end
+
+ it 'is not valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag)
+
+ expect(level).to be_invalid
+ expect(level.errors.full_messages).to include('Access level has already been taken')
+ end
+ end
+
+ context 'when a deploy key already added for this access level' do
+ let!(:create_access_level) do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ end
+
+ let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
+
+ it 'is not valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+
+ expect(level).to be_invalid
+ expect(level.errors.full_messages).to contain_exactly('Deploy key has already been taken')
+ end
+ end
+
+ context 'when deploy key is not enabled for the project' do
+ let(:create_access_level) do
+ build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: create(:deploy_key))
+ end
+
+ it 'returns an error' do
+ expect(create_access_level).to be_invalid
+ expect(create_access_level.errors.full_messages).to contain_exactly(
+ 'Deploy key is not enabled for this project'
+ )
+ end
+ end
+ end
+
+ describe '#check_access' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:deploy_key) { create(:deploy_key, user: user) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key, can_push: can_push)
+ end
+
+ let(:create_access_level) { protected_tag.create_access_levels.first }
+ let(:can_push) { true }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+
+ context 'when this create_access_level is tied to a deploy key' do
+ let(:create_access_level) do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ end
+
+ context 'when the deploy key is among the active keys for this project' do
+ it { expect(create_access_level.check_access(user)).to be_truthy }
+ end
+
+ context 'when user is missing' do
+ it { expect(create_access_level.check_access(nil)).to be_falsey }
+ end
+
+ context 'when deploy key does not belong to the user' do
+ let(:another_user) { create(:user) }
+
+ it { expect(create_access_level.check_access(another_user)).to be_falsey }
+ end
+
+ context 'when user cannot access the project' do
+ before do
+ allow(user).to receive(:can?).with(:read_project, project).and_return(false)
+ end
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+ end
+
+ context 'when the deploy key is not among the active keys of this project' do
+ let(:can_push) { false }
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+ end
+ end
+ end
+
+ describe '#type' do
+ let(:create_access_level) { build(:protected_tag_create_access_level) }
+
+ it 'returns :role by default' do
+ expect(create_access_level.type).to eq(:role)
+ end
+
+ context 'when a deploy key is tied to the protected branch' do
+ let(:create_access_level) { build(:protected_tag_create_access_level, deploy_key: build(:deploy_key)) }
+
+ it 'returns :deploy_key' do
+ expect(create_access_level.type).to eq(:deploy_key)
+ end
+ end
+ end
+end
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 4148452f849..0391acc3781 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
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)
+ allow(Dir).to receive(:glob).and_call_original
+ allow(Dir).to receive(:glob).with(described_class.whats_new_path).and_return(fixture_dir_glob)
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 5ed4eb7d233..880fb21b7af 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -71,25 +71,12 @@ RSpec.describe Release do
subject { build(:release, project: project, name: 'Release 1.0') }
it { is_expected.to validate_presence_of(:author_id) }
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(validate_release_with_author: false)
- end
-
- it { is_expected.not_to validate_presence_of(:author_id) }
- end
end
- # Mimic releases created before 11.7
- # See: https://gitlab.com/gitlab-org/gitlab/-/blob/8e5a110b01f842d8b6a702197928757a40ce9009/app/models/release.rb#L14
+ # Deleting user along with their contributions, nullifies releases author_id.
context 'when updating existing release without author' do
let(:release) { create(:release, :legacy) }
- before do
- stub_feature_flags(validate_release_with_author: false)
- end
-
it 'updates successfully' do
release.description += 'Update'
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a3d2f9a09fb..b8780b3faae 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -566,16 +566,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
expect(commit_ids).to include(*expected_commit_ids)
expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
end
-
- context 'when feature flag "commit_search_trailing_spaces" is disabled' do
- before do
- stub_feature_flags(commit_search_trailing_spaces: false)
- end
-
- it 'returns an empty list' do
- expect(commit_ids).to be_empty
- end
- end
end
describe 'when storage is broken', :broken_storage do
@@ -1858,6 +1848,8 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#expire_root_ref_cache' do
+ let(:project) { create(:project) }
+
it 'expires the root reference cache' do
repository.root_ref
@@ -1959,6 +1951,40 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
+ describe '#merge_to_branch' do
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature', target_branch: project.default_branch, source_project: project)
+ end
+
+ it 'merges two branches and returns the merge commit id' do
+ message = 'New merge commit'
+ merge_commit_id =
+ repository.merge_to_branch(user,
+ source_sha: merge_request.diff_head_sha,
+ target_branch: merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
+ message: message)
+
+ expect(repository.commit(merge_commit_id).message).to eq(message)
+ expect(repository.commit(merge_request.target_branch).sha).to eq(merge_commit_id)
+ end
+
+ it 'does not merge if target branch has been changed' do
+ target_sha = project.commit.sha
+
+ repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: project.default_branch)
+
+ merge_commit_id =
+ repository.merge_to_branch(user,
+ source_sha: merge_request.diff_head_sha,
+ target_branch: merge_request.target_branch,
+ target_sha: target_sha,
+ message: 'New merge commit')
+
+ expect(merge_commit_id).to be_nil
+ end
+ end
+
describe '#merge_to_ref' do
let(:merge_request) do
create(:merge_request, source_branch: 'feature',
@@ -1985,15 +2011,20 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#ff_merge' do
+ let(:target_branch) { 'ff-target' }
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature', target_branch: target_branch, source_project: project)
+ end
+
before do
- repository.add_branch(user, 'ff-target', 'feature~5')
+ repository.add_branch(user, target_branch, 'feature~5')
end
it 'merges the code and return the commit id' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
merge_commit = repository.commit(merge_commit_id)
@@ -2002,14 +2033,24 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
+
+ it 'does not merge if target branch has been changed' do
+ target_sha = project.commit(target_branch).sha
+
+ repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: target_branch)
+
+ merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, target_branch, target_sha: target_sha)
+
+ expect(merge_commit_id).to be_nil
+ end
end
describe '#rebase' do
@@ -2580,17 +2621,17 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:file_on_head)
- .with(:avatar)
- .and_return(double(:tree, path: 'logo.png'))
+ .with(:avatar)
+ .and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
end
it 'caches the output' do
expect(repository).to receive(:file_on_head)
- .with(:avatar)
- .once
- .and_return(double(:tree, path: 'logo.png'))
+ .with(:avatar)
+ .once
+ .and_return(double(:tree, path: 'logo.png'))
2.times { expect(repository.avatar).to eq('logo.png') }
end
@@ -2718,26 +2759,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'caches the response' do
- expect(repository).to receive(:search_files_by_regexp).and_call_original.once
+ expect(repository.head_tree).to receive(:readme_path).and_call_original.once
2.times do
expect(repository.readme_path).to eq("README.md")
end
end
-
- context 'when "readme_from_gitaly" FF is disabled' do
- before do
- stub_feature_flags(readme_from_gitaly: false)
- end
-
- it 'caches the response' do
- expect(repository.head_tree).to receive(:readme_path).and_call_original.once
-
- 2.times do
- expect(repository.readme_path).to eq("README.md")
- end
- end
- end
end
end
end
@@ -2803,7 +2830,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
context 'with a non-existing repository' do
it 'returns nil' do
- expect(repository).to receive(:head_commit).and_return(nil)
+ expect(repository).to receive(:root_ref).and_return(nil)
expect(repository.head_tree).to be_nil
end
@@ -2820,7 +2847,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
context 'using a non-existing repository' do
before do
- allow(repository).to receive(:head_commit).and_return(nil)
+ allow(repository).to receive(:root_ref).and_return(nil)
end
it { is_expected.to be_nil }
diff --git a/spec/models/resource_event_spec.rb b/spec/models/resource_event_spec.rb
index f40c192ab2b..62bd5314b69 100644
--- a/spec/models/resource_event_spec.rb
+++ b/spec/models/resource_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceEvent, feature_category: :team_planning, type: :model do
let(:dummy_resource_label_event_class) do
Class.new(ResourceEvent) do
self.table_name = 'resource_label_events'
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 87f3b9fb2bb..eb28010d57f 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceLabelEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceLabelEvent, feature_category: :team_planning, type: :model do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index 11b704ceadf..d237a16da8f 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceMilestoneEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceMilestoneEvent, feature_category: :team_planning, type: :model do
it_behaves_like 'a resource event'
it_behaves_like 'a resource event for issues'
it_behaves_like 'a resource event for merge requests'
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index 04e4359a3ff..a6d6b507b69 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceStateEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :model do
subject { build(:resource_state_event, issue: issue) }
let(:issue) { create(:issue) }
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index c1ec35732b8..32c36375a3d 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ServiceDeskSetting do
+RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
describe 'validations' do
subject(:service_desk_setting) { create(:service_desk_setting) }
@@ -12,6 +12,48 @@ RSpec.describe ServiceDeskSetting do
it { is_expected.to allow_value('abc123_').for(:project_key) }
it { is_expected.not_to allow_value('abc 12').for(:project_key).with_message("can contain only lowercase letters, digits, and '_'.") }
it { is_expected.not_to allow_value('Big val').for(:project_key) }
+ it { is_expected.to validate_length_of(:custom_email).is_at_most(255) }
+ it { is_expected.to validate_length_of(:custom_email_smtp_address).is_at_most(255) }
+ it { is_expected.to validate_length_of(:custom_email_smtp_username).is_at_most(255) }
+
+ describe '#custom_email_enabled' do
+ it { expect(subject.custom_email_enabled).to be_falsey }
+ it { expect(described_class.new(custom_email_enabled: true).custom_email_enabled).to be_truthy }
+ end
+
+ context 'when custom_email_enabled is true' do
+ before do
+ subject.custom_email_enabled = true
+ end
+
+ it { is_expected.to validate_presence_of(:custom_email) }
+ it { is_expected.to validate_uniqueness_of(:custom_email).allow_nil }
+ it { is_expected.to allow_value('support@example.com').for(:custom_email) }
+ it { is_expected.to allow_value('support@xn--brggen-4ya.de').for(:custom_email) } # converted domain name with umlaut
+ it { is_expected.to allow_value('support1@shop.example.com').for(:custom_email) }
+ it { is_expected.to allow_value('support-shop_with.crazy-address@shop.example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('support@example@example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('support.example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('example').for(:custom_email) }
+ it { is_expected.not_to allow_value('" "@example.org').for(:custom_email) }
+ it { is_expected.not_to allow_value('support+12@example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('user@[IPv6:2001:db8::1]').for(:custom_email) }
+ it { is_expected.not_to allow_value('"><script>alert(1);</script>"@example.org').for(:custom_email) }
+ it { is_expected.not_to allow_value('file://example').for(:custom_email) }
+ it { is_expected.not_to allow_value('no email at all').for(:custom_email) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_username) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_port) }
+ it { is_expected.to validate_numericality_of(:custom_email_smtp_port).only_integer.is_greater_than(0) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_address) }
+ it { is_expected.to allow_value('smtp.gmail.com').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('https://example.com').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('file://example').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('/example').for(:custom_email_smtp_address) }
+ end
describe '.valid_issue_template' do
let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) }
@@ -67,6 +109,27 @@ RSpec.describe ServiceDeskSetting do
end
end
+ describe 'encrypted password' do
+ let_it_be(:settings) do
+ create(
+ :service_desk_setting,
+ custom_email_enabled: true,
+ custom_email: 'supersupport@example.com',
+ custom_email_smtp_address: 'smtp.example.com',
+ custom_email_smtp_port: 587,
+ custom_email_smtp_username: 'supersupport@example.com',
+ custom_email_smtp_password: 'supersecret'
+ )
+ end
+
+ it 'saves and retrieves the encrypted custom email smtp password and iv correctly' do
+ expect(settings.encrypted_custom_email_smtp_password).not_to be_nil
+ expect(settings.encrypted_custom_email_smtp_password_iv).not_to be_nil
+
+ expect(settings.custom_email_smtp_password).to eq('supersecret')
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 1893b6530a5..7d433896cf8 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -38,6 +38,27 @@ RSpec.describe UserDetail do
it { is_expected.to validate_length_of(:skype).is_at_most(500) }
end
+ describe '#discord' do
+ it { is_expected.to validate_length_of(:discord).is_at_most(500) }
+
+ context 'when discord is set' do
+ let_it_be(:user_detail) { create(:user_detail) }
+
+ it 'accepts a valid discord user id' do
+ user_detail.discord = '1234567890123456789'
+
+ expect(user_detail).to be_valid
+ end
+
+ it 'throws an error when other url format is wrong' do
+ user_detail.discord = '123456789'
+
+ expect(user_detail).not_to be_valid
+ expect(user_detail.errors.full_messages).to match_array([_('Discord must contain only a discord user ID.')])
+ end
+ end
+ end
+
describe '#location' do
it { is_expected.to validate_length_of(:location).is_at_most(500) }
end
@@ -72,11 +93,12 @@ RSpec.describe UserDetail do
let(:user_detail) do
create(:user_detail,
bio: 'bio',
+ discord: '1234567890123456789',
linkedin: 'linkedin',
- twitter: 'twitter',
- skype: 'skype',
location: 'location',
organization: 'organization',
+ skype: 'skype',
+ twitter: 'twitter',
website_url: 'https://example.com')
end
@@ -90,11 +112,12 @@ RSpec.describe UserDetail do
end
it_behaves_like 'prevents `nil` value', :bio
+ it_behaves_like 'prevents `nil` value', :discord
it_behaves_like 'prevents `nil` value', :linkedin
- it_behaves_like 'prevents `nil` value', :twitter
- it_behaves_like 'prevents `nil` value', :skype
it_behaves_like 'prevents `nil` value', :location
it_behaves_like 'prevents `nil` value', :organization
+ it_behaves_like 'prevents `nil` value', :skype
+ it_behaves_like 'prevents `nil` value', :twitter
it_behaves_like 'prevents `nil` value', :website_url
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e2e4e4248d8..e87667d9604 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe User, feature_category: :users do
+RSpec.describe User, feature_category: :user_profile do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
@@ -102,6 +102,9 @@ RSpec.describe User, feature_category: :users do
it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
+ it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil }
+
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -2126,6 +2129,7 @@ RSpec.describe User, feature_category: :users do
expect(user.encrypted_otp_secret_salt).to be_nil
expect(user.otp_backup_codes).to be_nil
expect(user.otp_grace_period_started_at).to be_nil
+ expect(user.otp_secret_expires_at).to be_nil
end
end
@@ -2401,21 +2405,6 @@ RSpec.describe User, feature_category: :users do
it_behaves_like 'manageable groups examples'
end
end
-
- describe '#manageable_groups_with_routes' do
- it 'eager loads routes from manageable groups' do
- control_count =
- ActiveRecord::QueryRecorder.new(skip_cached: false) do
- user.manageable_groups_with_routes.map(&:route)
- end.count
-
- create(:group, parent: subgroup)
-
- expect do
- user.manageable_groups_with_routes.map(&:route)
- end.not_to exceed_all_query_limit(control_count)
- end
- end
end
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 44c6f6c9c1a..1b177934ace 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -13,15 +13,20 @@ RSpec.describe WikiDirectory do
let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') }
let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') }
let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') }
+ let_it_be(:parent1) { build(:wiki_page, title: 'parent1') }
+ let_it_be(:parent2) { build(:wiki_page, title: 'parent2') }
let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') }
let_it_be(:child2) { build(:wiki_page, title: 'parent1/child2') }
let_it_be(:child3) { build(:wiki_page, title: 'parent2/child3') }
+ let_it_be(:subparent) { build(:wiki_page, title: 'parent1/subparent') }
let_it_be(:grandchild1) { build(:wiki_page, title: 'parent1/subparent/grandchild1') }
let_it_be(:grandchild2) { build(:wiki_page, title: 'parent1/subparent/grandchild2') }
it 'returns a nested array of entries' do
entries = described_class.group_pages(
- [toplevel1, toplevel2, toplevel3, child1, child2, child3, grandchild1, grandchild2].sort_by(&:title)
+ [toplevel1, toplevel2, toplevel3,
+ parent1, parent2, child1, child2, child3,
+ subparent, grandchild1, grandchild2].sort_by(&:title)
)
expect(entries).to match(
@@ -95,7 +100,7 @@ RSpec.describe WikiDirectory do
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
- expect(directory.to_partial_path).to eq('../shared/wikis/wiki_directory')
+ expect(directory.to_partial_path).to eq('shared/wikis/wiki_directory')
end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index fcb041aebe5..21da06a222f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -912,7 +912,7 @@ RSpec.describe WikiPage do
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
- expect(build_wiki_page(container).to_partial_path).to eq('../shared/wikis/wiki_page')
+ expect(build_wiki_page(container).to_partial_path).to eq('shared/wikis/wiki_page')
end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 0bedcc9791f..6aacaa3c119 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -21,9 +21,8 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
.with_foreign_key('work_item_id')
end
- it 'has many `work_item_children_by_created_at`' do
- is_expected.to have_many(:work_item_children_by_created_at)
- .order(created_at: :asc)
+ it 'has many `work_item_children_by_relative_position`' do
+ is_expected.to have_many(:work_item_children_by_relative_position)
.class_name('WorkItem')
.with_foreign_key('work_item_id')
end
@@ -35,6 +34,49 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '.work_item_children_by_relative_position' do
+ subject { parent_item.reload.work_item_children_by_relative_position }
+
+ let_it_be(:parent_item) { create(:work_item, :objective, project: reusable_project) }
+ let_it_be(:oldest_item) { create(:work_item, :objective, created_at: 5.hours.ago, project: reusable_project) }
+ let_it_be(:middle_item) { create(:work_item, :objective, project: reusable_project) }
+ let_it_be(:newest_item) { create(:work_item, :objective, created_at: 5.hours.from_now, project: reusable_project) }
+
+ let_it_be_with_reload(:link_to_oldest_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: oldest_item)
+ end
+
+ let_it_be_with_reload(:link_to_middle_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: middle_item)
+ end
+
+ let_it_be_with_reload(:link_to_newest_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: newest_item)
+ end
+
+ context 'when ordered by relative position and created_at' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:oldest_item_position, :middle_item_position, :newest_item_position, :expected_order) do
+ nil | nil | nil | lazy { [oldest_item, middle_item, newest_item] }
+ nil | nil | 1 | lazy { [newest_item, oldest_item, middle_item] }
+ nil | 1 | 2 | lazy { [middle_item, newest_item, oldest_item] }
+ 2 | 3 | 1 | lazy { [newest_item, oldest_item, middle_item] }
+ 1 | 2 | 3 | lazy { [oldest_item, middle_item, newest_item] }
+ end
+
+ with_them do
+ before do
+ link_to_oldest_item.update!(relative_position: oldest_item_position)
+ link_to_middle_item.update!(relative_position: middle_item_position)
+ link_to_newest_item.update!(relative_position: newest_item_position)
+ end
+
+ it { is_expected.to eq(expected_order) }
+ end
+ end
+ end
+
describe '#noteable_target_type_name' do
it 'returns `issue` as the target name' do
work_item = build(:work_item)
@@ -57,6 +99,70 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#supports_assignee?' do
+ let(:work_item) { build(:work_item, :task) }
+
+ before do
+ allow(work_item.work_item_type).to receive(:supports_assignee?).and_return(false)
+ end
+
+ it 'delegates the call to its work item type' do
+ expect(work_item.supports_assignee?).to be(false)
+ end
+ end
+
+ describe '#supported_quick_action_commands' do
+ let(:work_item) { build(:work_item, :task) }
+
+ subject { work_item.supported_quick_action_commands }
+
+ it 'returns quick action commands supported for all work items' do
+ is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug)
+ end
+
+ context 'when work item supports the assignee widget' do
+ it 'returns assignee related quick action commands' do
+ is_expected.to include(:assign, :unassign, :reassign)
+ end
+ end
+
+ context 'when work item does not the assignee widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits assignee related quick action commands' do
+ is_expected.not_to include(:assign, :unassign, :reassign)
+ end
+ end
+
+ context 'when work item supports the labels widget' do
+ it 'returns labels related quick action commands' do
+ is_expected.to include(:label, :labels, :relabel, :remove_label, :unlabel)
+ end
+ end
+
+ context 'when work item does not support the labels widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits labels related quick action commands' do
+ is_expected.not_to include(:label, :labels, :relabel, :remove_label, :unlabel)
+ end
+ end
+
+ context 'when work item supports the start and due date widget' do
+ it 'returns due date related quick action commands' do
+ is_expected.to include(:due, :remove_due_date)
+ end
+ end
+
+ context 'when work item does not support the start and due date widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits due date related quick action commands' do
+ is_expected.not_to include(:due, :remove_due_date)
+ end
+ end
+ end
+
describe 'callbacks' do
describe 'record_create_action' do
it 'records the creation action after saving' do
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 1ada783385e..e5c88634b26 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -10,6 +10,20 @@ RSpec.describe WorkItems::Type do
describe 'associations' do
it { is_expected.to have_many(:work_items).with_foreign_key('work_item_type_id') }
it { is_expected.to belong_to(:namespace) }
+
+ it 'has many `widget_definitions`' do
+ is_expected.to have_many(:widget_definitions)
+ .class_name('::WorkItems::WidgetDefinition')
+ .with_foreign_key('work_item_type_id')
+ end
+
+ it 'has many `enabled_widget_definitions`' do
+ type = create(:work_item_type)
+ widget1 = create(:widget_definition, work_item_type: type)
+ create(:widget_definition, work_item_type: type, disabled: true)
+
+ expect(type.enabled_widget_definitions).to match_array([widget1])
+ end
end
describe 'scopes' do
@@ -60,29 +74,14 @@ RSpec.describe WorkItems::Type do
it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
end
- describe '.available_widgets' do
- subject { described_class.available_widgets }
-
- it 'returns list of all possible widgets' do
- is_expected.to include(
- ::WorkItems::Widgets::Description,
- ::WorkItems::Widgets::Hierarchy,
- ::WorkItems::Widgets::Labels,
- ::WorkItems::Widgets::Assignees,
- ::WorkItems::Widgets::StartAndDueDate,
- ::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::BaseTypeImporter).not_to receive(:upsert_types).and_call_original
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_widgets)
expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions)
expect(subject).to eq(default_issue_type)
@@ -94,7 +93,8 @@ RSpec.describe WorkItems::Type do
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::BaseTypeImporter).to receive(:upsert_types).and_call_original
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_widgets)
expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions)
expect(subject).to eq(default_issue_type)
@@ -126,4 +126,41 @@ RSpec.describe WorkItems::Type do
expect(work_item_type.name).to eq('label😸')
end
end
+
+ describe '#supports_assignee?' do
+ let_it_be_with_reload(:work_item_type) { create(:work_item_type) }
+ let_it_be_with_reload(:widget_definition) do
+ create(:widget_definition, work_item_type: work_item_type, widget_type: :assignees)
+ end
+
+ subject(:supports_assignee) { work_item_type.supports_assignee? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when the assignees widget is not supported' do
+ before do
+ widget_definition.update!(disabled: true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#default_issue?' do
+ context 'when work item type is default Issue' do
+ let(:work_item_type) { build(:work_item_type, name: described_class::TYPE_NAMES[:issue]) }
+
+ it 'returns true' do
+ expect(work_item_type.default_issue?).to be(true)
+ end
+ end
+
+ context 'when work item type is not Issue' do
+ let(:work_item_type) { build(:work_item_type) }
+
+ it 'returns false' do
+ expect(work_item_type.default_issue?).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
new file mode 100644
index 00000000000..08f8f4d9663
--- /dev/null
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
+ let(:all_widget_classes) do
+ list = [
+ ::WorkItems::Widgets::Description,
+ ::WorkItems::Widgets::Hierarchy,
+ ::WorkItems::Widgets::Labels,
+ ::WorkItems::Widgets::Assignees,
+ ::WorkItems::Widgets::StartAndDueDate,
+ ::WorkItems::Widgets::Milestone,
+ ::WorkItems::Widgets::Notes
+ ]
+
+ if Gitlab.ee?
+ list += [
+ ::WorkItems::Widgets::Iteration,
+ ::WorkItems::Widgets::Weight,
+ ::WorkItems::Widgets::Status,
+ ::WorkItems::Widgets::HealthStatus,
+ ::WorkItems::Widgets::Progress,
+ ::WorkItems::Widgets::RequirementLegacy,
+ ::WorkItems::Widgets::TestReports
+ ]
+ end
+
+ list
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace) }
+ it { is_expected.to belong_to(:work_item_type) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id, :work_item_type_id]) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ end
+
+ context 'with some widgets disabled' do
+ before do
+ described_class.global.where(widget_type: :notes).update_all(disabled: true)
+ end
+
+ describe '.available_widgets' do
+ subject { described_class.available_widgets }
+
+ it 'returns all global widgets excluding the disabled ones' do
+ # WorkItems::Widgets::Notes is excluded from widget class because:
+ # * although widget_definition below is enabled and uses notes widget, it's namespaced (has namespace != nil)
+ # * available_widgets takes into account only global definitions (which have namespace=nil)
+ namespace = create(:namespace)
+ create(:widget_definition, namespace: namespace, widget_type: :notes)
+
+ is_expected.to match_array(all_widget_classes - [::WorkItems::Widgets::Notes])
+ end
+
+ it 'returns all global widgets if there is at least one global widget definition which is enabled' do
+ create(:widget_definition, namespace: nil, widget_type: :notes)
+
+ is_expected.to match_array(all_widget_classes)
+ end
+ end
+
+ describe '.widget_classes' do
+ subject { described_class.widget_classes }
+
+ it 'returns all widget classes no matter if disabled or not' do
+ is_expected.to match_array(all_widget_classes)
+ end
+ end
+ end
+
+ describe '#widget_class' do
+ it 'returns widget class based on widget_type' do
+ expect(build(:widget_definition, widget_type: :description).widget_class).to eq(::WorkItems::Widgets::Description)
+ end
+
+ it 'returns nil if there is no class for the widget_type' do
+ described_class.first.update_column(:widget_type, -1)
+
+ expect(described_class.first.widget_class).to be_nil
+ end
+
+ it 'returns nil if there is no class for the widget_type' do
+ expect(build(:widget_definition, widget_type: nil).widget_class).to be_nil
+ end
+ end
+end
diff --git a/spec/models/work_items/widgets/assignees_spec.rb b/spec/models/work_items/widgets/assignees_spec.rb
index a2c93c07fde..19c17658ce4 100644
--- a/spec/models/work_items/widgets/assignees_spec.rb
+++ b/spec/models/work_items/widgets/assignees_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::Assignees do
it { is_expected.to eq(:assignees) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:assignee_ids) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }
diff --git a/spec/models/work_items/widgets/hierarchy_spec.rb b/spec/models/work_items/widgets/hierarchy_spec.rb
index 43670b30645..7ff3088d9ec 100644
--- a/spec/models/work_items/widgets/hierarchy_spec.rb
+++ b/spec/models/work_items/widgets/hierarchy_spec.rb
@@ -36,14 +36,40 @@ RSpec.describe WorkItems::Widgets::Hierarchy, feature_category: :team_planning d
it { is_expected.to contain_exactly(parent_link1.work_item, parent_link2.work_item) }
- context 'with default order by created_at' do
+ context 'when ordered by relative position and created_at' do
let_it_be(:oldest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) }
+ let_it_be(:newest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
let_it_be_with_reload(:link_to_oldest_child) do
create(:parent_link, work_item_parent: work_item_parent, work_item: oldest_child)
end
- it { is_expected.to eq([link_to_oldest_child, parent_link1, parent_link2].map(&:work_item)) }
+ let_it_be_with_reload(:link_to_newest_child) do
+ create(:parent_link, work_item_parent: work_item_parent, work_item: newest_child)
+ end
+
+ let(:parent_links_ordered) { [link_to_oldest_child, parent_link1, parent_link2, link_to_newest_child] }
+
+ context 'when children relative positions are nil' do
+ it 'orders by created_at' do
+ is_expected.to eq(parent_links_ordered.map(&:work_item))
+ end
+ end
+
+ context 'when children relative positions are present' do
+ let(:first_position) { 10 }
+ let(:second_position) { 20 }
+ let(:parent_links_ordered) { [link_to_oldest_child, link_to_newest_child, parent_link1, parent_link2] }
+
+ before do
+ link_to_oldest_child.update!(relative_position: first_position)
+ link_to_newest_child.update!(relative_position: second_position)
+ end
+
+ it 'orders by relative_position and by created_at' do
+ is_expected.to eq(parent_links_ordered.map(&:work_item))
+ end
+ end
end
end
end
diff --git a/spec/models/work_items/widgets/labels_spec.rb b/spec/models/work_items/widgets/labels_spec.rb
index 15e8aaa1cf3..8640c39c146 100644
--- a/spec/models/work_items/widgets/labels_spec.rb
+++ b/spec/models/work_items/widgets/labels_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::Labels do
it { is_expected.to eq(:labels) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:add_label_ids, :remove_label_ids, :label_ids) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }
diff --git a/spec/models/work_items/widgets/start_and_due_date_spec.rb b/spec/models/work_items/widgets/start_and_due_date_spec.rb
index b023cc73e0f..568d960c9c7 100644
--- a/spec/models/work_items/widgets/start_and_due_date_spec.rb
+++ b/spec/models/work_items/widgets/start_and_due_date_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::StartAndDueDate do
it { is_expected.to eq(:start_and_due_date) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:due_date) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }
diff --git a/spec/policies/ci/runner_policy_spec.rb b/spec/policies/ci/runner_policy_spec.rb
index 6039d60ec2f..e0a9e3c2870 100644
--- a/spec/policies/ci/runner_policy_spec.rb
+++ b/spec/policies/ci/runner_policy_spec.rb
@@ -3,11 +3,12 @@
require 'spec_helper'
RSpec.describe Ci::RunnerPolicy, feature_category: :runner do
+ let_it_be(:owner) { create(:user) }
+
describe 'ability :read_runner' do
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
- let_it_be(:owner) { create(:user) }
let_it_be_with_reload(:group) { create(:group, name: 'top-level', path: 'top-level') }
let_it_be_with_reload(:subgroup) { create(:group, name: 'subgroup', path: 'subgroup', parent: group) }
@@ -170,4 +171,24 @@ RSpec.describe Ci::RunnerPolicy, feature_category: :runner do
end
end
end
+
+ describe 'ability :read_ephemeral_token' do
+ subject(:policy) { described_class.new(user, runner) }
+
+ let_it_be(:runner) { create(:ci_runner, creator: owner) }
+
+ let(:creator) { owner }
+
+ context 'with request made by creator' do
+ let(:user) { creator }
+
+ it { expect_allowed :read_ephemeral_token }
+ end
+
+ context 'with request made by another user' do
+ let(:user) { create(:admin) }
+
+ it { expect_disallowed :read_ephemeral_token }
+ end
+ end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 1538f8a70c8..0575ba3237b 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GlobalPolicy, feature_category: :security_policies do
+RSpec.describe GlobalPolicy, feature_category: :shared do
include TermsHelper
let_it_be(:admin_user) { create(:admin) }
@@ -591,4 +591,102 @@ RSpec.describe GlobalPolicy, feature_category: :security_policies do
it { is_expected.to be_disallowed(:log_in) }
end
end
+
+ describe 'create_instance_runners' do
+ context 'create_runner_workflow flag enabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: true)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin_user }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_instance_runners) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+ end
+
+ context 'with project_bot' do
+ let(:current_user) { project_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with migration_bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with security_bot' do
+ let(:current_user) { security_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with regular user' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+ end
+
+ context 'create_runner_workflow flag disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: false)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin_user }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+ end
+
+ context 'with project_bot' do
+ let(:current_user) { project_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with migration_bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with security_bot' do
+ let(:current_user) { security_bot }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with regular user' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_instance_runners) }
+ end
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 2d4c86845c9..451db9eaf9c 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GroupPolicy do
+RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization do
include AdminModeHelper
include_context 'GroupPolicy context'
@@ -1274,6 +1274,178 @@ RSpec.describe GroupPolicy do
end
end
+ describe 'create_group_runners' do
+ shared_examples 'disallowed when group runner registration disabled' do
+ context 'with group runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['project'])
+ group.runner_registration_enabled = runner_registration_enabled
+ end
+
+ context 'with specific group runner registration enabled' do
+ let(:runner_registration_enabled) { true }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with specific group runner registration disabled' do
+ let(:runner_registration_enabled) { false }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+ end
+ end
+
+ context 'create_runner_workflow flag enabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: true)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_group_runners) }
+
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
+
+ it { is_expected.to be_allowed(:create_group_runners) }
+ end
+
+ context 'with group runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['project'])
+ group.runner_registration_enabled = runner_registration_enabled
+ end
+
+ context 'with specific group runner registration enabled' do
+ let(:runner_registration_enabled) { true }
+
+ it { is_expected.to be_allowed(:create_group_runners) }
+ end
+
+ context 'with specific group runner registration disabled' do
+ let(:runner_registration_enabled) { false }
+
+ it { is_expected.to be_allowed(:create_group_runners) }
+ end
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_group_runners) }
+
+ it_behaves_like 'disallowed when group runner registration disabled'
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+ end
+
+ context 'with create_runner_workflow flag disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: false)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_disallowed(:create_group_runners) }
+
+ context 'with specific group runner registration disabled' do
+ before do
+ group.runner_registration_enabled = false
+ end
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ it_behaves_like 'disallowed when group runner registration disabled'
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+
+ it_behaves_like 'disallowed when group runner registration disabled'
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_group_runners) }
+ end
+ end
+ end
+
describe 'read_group_all_available_runners' do
context 'admin' do
let(:current_user) { admin }
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 0040d9dff7e..17558787966 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -425,19 +425,15 @@ RSpec.describe IssuePolicy, feature_category: :team_planning do
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) }
+ context 'and notes widget is disabled for issue' do
+ before do
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
+ end
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_internal, :read_internal_note)
- expect(permissions(admin, task)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
+ expect(permissions(maintainer, issue)).to be_disallowed(:create_note, :read_note, :mark_note_as_internal, :read_internal_note)
+ expect(permissions(admin, issue)).to be_disallowed(:create_note, :read_note, :read_internal_note, :mark_note_as_internal, :set_note_created_at)
end
end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index f4abe3a223c..b2191e6925d 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -260,9 +260,7 @@ RSpec.describe NotePolicy, feature_category: :team_planning 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)
+ WorkItems::Type.default_by_type(:task).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
context 'when noteable is task' do
diff --git a/spec/policies/packages/policies/project_policy_spec.rb b/spec/policies/packages/policies/project_policy_spec.rb
index 5d54ee54572..5c267ff5ac5 100644
--- a/spec/policies/packages/policies/project_policy_spec.rb
+++ b/spec/policies/packages/policies/project_policy_spec.rb
@@ -122,39 +122,6 @@ RSpec.describe Packages::Policies::ProjectPolicy do
end
end
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(package_registry_access_level: false)
- end
-
- where(:project, :current_user, :expect_to_be_allowed) do
- ref(:private_project) | ref(:anonymous) | false
- ref(:private_project) | ref(:non_member) | false
- ref(:private_project) | ref(:guest) | false
- ref(:internal_project) | ref(:anonymous) | false
- ref(:public_project) | ref(:admin) | true
- ref(:public_project) | ref(:owner) | true
- ref(:public_project) | ref(:maintainer) | true
- ref(:public_project) | ref(:developer) | true
- ref(:public_project) | ref(:reporter) | true
- ref(:public_project) | ref(:guest) | true
- ref(:public_project) | ref(:non_member) | true
- ref(:public_project) | ref(:anonymous) | true
- end
-
- with_them do
- it do
- project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
-
- if expect_to_be_allowed
- is_expected.to be_allowed(:read_package)
- else
- is_expected.to be_disallowed(:read_package)
- end
- end
- end
- end
-
context 'with admin' do
let(:current_user) { admin }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a98f091b9fc..b2fb310aca3 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -2478,7 +2478,14 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
before do
current_user.set_ci_job_token_scope!(job)
current_user.external = external_user
- scope_project.update!(ci_outbound_job_token_scope_enabled: token_scope_enabled)
+ project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
+ scope_project.update!(
+ ci_outbound_job_token_scope_enabled: token_scope_enabled,
+ ci_inbound_job_token_scope_enabled: token_scope_enabled
+ )
end
it "enforces the expected permissions" do
@@ -2732,6 +2739,148 @@ RSpec.describe ProjectPolicy, feature_category: :authentication_and_authorizatio
end
end
+ describe 'create_project_runners' do
+ context 'create_runner_workflow flag enabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: true)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:create_project_runners) }
+
+ context 'with project runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ it { is_expected.to be_allowed(:create_project_runners) }
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_project_runners) }
+
+ context 'with project runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_project_runners) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'create_runner_workflow flag disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow: false)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_disallowed(:create_project_runners) }
+
+ context 'with project runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+
+ context 'with project runner registration disabled' do
+ before do
+ stub_application_setting(valid_runner_registrars: ['group'])
+ end
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+
+ context 'with maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:create_project_runners) }
+ end
+ end
+ end
+
describe 'update_sentry_issue' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/policies/todo_policy_spec.rb b/spec/policies/todo_policy_spec.rb
index fa62f53c628..0230f106f0f 100644
--- a/spec/policies/todo_policy_spec.rb
+++ b/spec/policies/todo_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodoPolicy, feature_category: :project_management do
+RSpec.describe TodoPolicy, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
diff --git a/spec/presenters/issue_email_participant_presenter_spec.rb b/spec/presenters/issue_email_participant_presenter_spec.rb
new file mode 100644
index 00000000000..c270fae3058
--- /dev/null
+++ b/spec/presenters/issue_email_participant_presenter_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssueEmailParticipantPresenter, feature_category: :service_desk do
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/389247
+ # for details around build_stubbed for access level
+ let_it_be(:non_member) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:guest) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:reporter) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:developer) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:project) { create(:project, group: group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:issue) { build_stubbed(:issue, project: project) }
+ let_it_be(:participant) { build_stubbed(:issue_email_participant, issue: issue, email: 'any@email.com') }
+
+ let(:user) { nil }
+ let(:presenter) { described_class.new(participant, current_user: user) }
+ let(:obfuscated_email) { 'an*****@e*****.c**' }
+ let(:email) { 'any@email.com' }
+
+ before_all do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ end
+
+ describe '#email' do
+ subject { presenter.email }
+
+ it { is_expected.to eq(obfuscated_email) }
+
+ context 'with signed in user' do
+ context 'when user has no role in project' do
+ let(:user) { non_member }
+
+ it { is_expected.to eq(obfuscated_email) }
+ end
+
+ context 'when user has guest role in project' do
+ let(:user) { guest }
+
+ it { is_expected.to eq(obfuscated_email) }
+ end
+
+ context 'when user has reporter role in project' do
+ let(:user) { reporter }
+
+ it { is_expected.to eq(email) }
+ end
+
+ context 'when user has developer role in project' do
+ let(:user) { developer }
+
+ it { is_expected.to eq(email) }
+ end
+ end
+ end
+end
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
index df43b0279dd..22a86d04a5a 100644
--- a/spec/presenters/issue_presenter_spec.rb
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -6,16 +6,25 @@ RSpec.describe IssuePresenter do
include Gitlab::Routing.url_helpers
let_it_be(:user) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:task) { create(:issue, :task, project: project) }
+ let_it_be(:non_member) { create(:user) }
let(:presented_issue) { issue }
let(:presenter) { described_class.new(presented_issue, current_user: user) }
+ let(:obfuscated_email) { 'an*****@e*****.c**' }
+ let(:email) { 'any@email.com' }
before_all do
group.add_developer(user)
+ group.add_developer(developer)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
end
describe '#web_url' do
@@ -99,4 +108,69 @@ RSpec.describe IssuePresenter do
it { is_expected.to be(true) }
end
end
+
+ describe '#service_desk_reply_to' do
+ context 'when issue is not a service desk issue' do
+ subject { presenter.service_desk_reply_to }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when issue is a service desk issue' do
+ let(:service_desk_issue) do
+ create(:issue, project: project, author: User.support_bot, service_desk_reply_to: email)
+ end
+
+ let(:user) { nil }
+
+ subject { described_class.new(service_desk_issue, current_user: user).service_desk_reply_to }
+
+ it { is_expected.to eq obfuscated_email }
+
+ context 'with signed in user' do
+ context 'when user has no role in project' do
+ let(:user) { non_member }
+
+ it { is_expected.to eq obfuscated_email }
+ end
+
+ context 'when user has guest role in project' do
+ let(:user) { guest }
+
+ it { is_expected.to eq obfuscated_email }
+ end
+
+ context 'when user has reporter role in project' do
+ let(:user) { reporter }
+
+ it { is_expected.to eq email }
+ end
+
+ context 'when user has developer role in project' do
+ let(:user) { developer }
+
+ it { is_expected.to eq email }
+ end
+ end
+ end
+ end
+
+ describe '#issue_email_participants' do
+ let(:participants_issue) { create(:issue, project: project) }
+
+ subject { described_class.new(participants_issue, current_user: user).issue_email_participants }
+
+ it { is_expected.to be_empty }
+
+ context "when an issue email participant exists" do
+ before do
+ participants_issue.issue_email_participants.create!(email: email)
+ end
+
+ it "has one element that is a presenter" do
+ expect(subject.size).to eq(1)
+ expect(subject.first).to be_a(IssueEmailParticipantPresenter)
+ end
+ end
+ end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index e3221c18afc..c4dfa73f648 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -108,6 +108,19 @@ RSpec.describe ProjectPresenter do
link: presenter.project_releases_path(project)
)
end
+
+ it 'returns environments anchor' do
+ environment = create(:environment, project: project)
+ unavailable_environment = create(:environment, project: project)
+ unavailable_environment.stop
+
+ expect(environment).to be_truthy
+ expect(presenter.environments_anchor_data).to have_attributes(
+ is_link: true,
+ label: a_string_including(project.environments.available.count.to_s),
+ link: presenter.project_environments_path(project)
+ )
+ end
end
end
@@ -632,6 +645,58 @@ RSpec.describe ProjectPresenter do
end
end
end
+
+ describe '#wiki_anchor_data' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:anchor_goto_wiki) do
+ have_attributes(
+ is_link: false,
+ label: a_string_ending_with('Wiki'),
+ link: wiki_path(project.wiki),
+ class_modifier: 'btn-default'
+ )
+ end
+
+ let(:anchor_add_wiki) do
+ have_attributes(
+ is_link: false,
+ label: a_string_ending_with('Add Wiki'),
+ link: "#{wiki_path(project.wiki)}?view=create"
+ )
+ end
+
+ where(:wiki_enabled, :can_read_wiki, :has_home_page, :can_create_wiki, :expected_result) do
+ true | true | true | true | ref(:anchor_goto_wiki)
+ true | true | true | false | ref(:anchor_goto_wiki)
+ true | true | false | true | ref(:anchor_add_wiki)
+ true | true | false | false | nil
+ true | false | true | true | nil
+ true | false | true | false | nil
+ true | false | false | true | nil
+ true | false | false | false | nil
+ false | true | true | true | nil
+ false | true | true | false | nil
+ false | true | true | false | nil
+ false | true | false | true | nil
+ false | true | false | false | nil
+ false | false | true | true | nil
+ false | false | true | false | nil
+ false | false | false | true | nil
+ false | false | false | false | nil
+ end
+
+ with_them do
+ before do
+ allow(project).to receive(:wiki_enabled?).and_return(wiki_enabled)
+ allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(can_read_wiki)
+ allow(project.wiki).to receive(:has_home_page?).and_return(has_home_page)
+ allow(presenter).to receive(:can?).with(user, :create_wiki, project).and_return(can_create_wiki)
+ end
+
+ it { expect(presenter.wiki_anchor_data).to match(expected_result) }
+ end
+ end
end
describe '#statistics_buttons' do
diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb
index 883eec68304..d1124d73dbd 100644
--- a/spec/presenters/user_presenter_spec.rb
+++ b/spec/presenters/user_presenter_spec.rb
@@ -17,6 +17,14 @@ RSpec.describe UserPresenter do
it { expect(presenter.web_url).to eq("http://localhost/#{user.username}") }
end
+ describe '#can?' do
+ it 'forwards call to the given user' do
+ expect(user).to receive(:can?).with("a", b: 24)
+
+ presenter.send(:can?, "a", b: 24)
+ end
+ end
+
context 'Gitpod' do
let(:gitpod_url) { "https://gitpod.io" }
let(:gitpod_application_enabled) { true }
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 49a80689c65..934f123e45b 100644
--- a/spec/requests/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -5,9 +5,12 @@ require 'spec_helper'
RSpec.describe AbuseReportsController, feature_category: :insider_threat do
let(:reporter) { create(:user) }
let(:user) { create(:user) }
+ let(:abuse_category) { 'spam' }
+
let(:attrs) do
attributes_for(:abuse_report) do |hash|
hash[:user_id] = user.id
+ hash[:category] = abuse_category
end
end
@@ -55,8 +58,6 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
describe 'POST add_category', :aggregate_failures do
subject(:request) { post add_category_abuse_reports_path, params: request_params }
- let(:abuse_category) { 'spam' }
-
context 'when user is reported for abuse' do
let(:ref_url) { 'http://example.com' }
let(:request_params) do
@@ -80,6 +81,17 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
reported_from_url: ref_url
)
end
+
+ it 'tracks the snowplow event' do
+ subject
+
+ expect_snowplow_event(
+ category: 'ReportAbuse',
+ action: 'select_abuse_category',
+ property: abuse_category,
+ user: user
+ )
+ end
end
context 'when abuse_report is missing in params' do
@@ -149,15 +161,35 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
expect(response).to redirect_to root_path
end
+
+ it 'tracks the snowplow event' do
+ post abuse_reports_path(abuse_report: attrs)
+
+ expect_snowplow_event(
+ category: 'ReportAbuse',
+ action: 'submit_form',
+ property: abuse_category,
+ user: user
+ )
+ end
end
context 'with invalid attributes' do
- it 'redirects back to root' do
+ before do
attrs.delete(:user_id)
+ end
+
+ it 'redirects back to root' do
post abuse_reports_path(abuse_report: attrs)
expect(response).to redirect_to root_path
end
+
+ it 'does not track the snowplow event' do
+ post abuse_reports_path(abuse_report: attrs)
+
+ expect_no_snowplow_event
+ end
end
end
end
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb
index db3e2fa0df6..88d81766e67 100644
--- a/spec/requests/admin/background_migrations_controller_spec.rb
+++ b/spec/requests/admin/background_migrations_controller_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode, featur
it 'returns CI database records' do
# If we only have one DB we'll see both migrations
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
ci_database_migration = Gitlab::Database::SharedModel.using_connection(ci_model.connection) { create(:batched_background_migration, :active) }
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
index 9712777d261..d946ac17f3f 100644
--- a/spec/requests/api/admin/batched_background_migrations_spec.rb
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when multiple database is enabled' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
let(:ci_model) { Ci::ApplicationRecord }
@@ -121,7 +121,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
it 'returns CI database records' do
# If we only have one DB we'll see both migrations
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
ci_database_migration = Gitlab::Database::SharedModel.using_connection(ci_model.connection) do
create(:batched_background_migration, :active, gitlab_schema: schema)
@@ -194,7 +194,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let(:database) { :ci }
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'uses the correct connection' do
@@ -262,7 +262,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let(:database) { :ci }
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'uses the correct connection' do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 9cf9c313f11..35851fff6c8 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -12,8 +12,22 @@ RSpec.describe API::API, feature_category: :authentication_and_authorization do
let(:user) { create(:user, last_activity_on: Date.yesterday) }
it 'updates the users last_activity_on to the current date' do
+ expect(Users::ActivityService).to receive(:new).with(author: user, project: nil, namespace: nil).and_call_original
+
expect { get api('/groups', user) }.to change { user.reload.last_activity_on }.to(Date.today)
end
+
+ context "with a project-specific path" do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { project.first_owner }
+
+ it "passes correct arguments to ActivityService" do
+ activity_args = { author: user, project: project, namespace: project.group }
+ expect(Users::ActivityService).to receive(:new).with(activity_args).and_call_original
+
+ get(api("/projects/#{project.id}/issues", user))
+ end
+ end
end
describe 'User with only read_api scope personal access token' do
@@ -171,7 +185,7 @@ RSpec.describe API::API, feature_category: :authentication_and_authorization do
'meta.remote_ip' => an_instance_of(String),
'meta.client_id' => a_string_matching(%r{\Auser/.+}),
'meta.user' => user.username,
- 'meta.feature_category' => 'users',
+ 'meta.feature_category' => 'user_profile',
'route' => '/api/:version/users')
expect(data.stringify_keys).not_to include('meta.caller_id')
@@ -312,4 +326,37 @@ RSpec.describe API::API, feature_category: :authentication_and_authorization do
end
end
end
+
+ describe 'admin mode support' do
+ let(:admin) { create(:admin) }
+
+ subject do
+ get api("/admin/clusters", personal_access_token: token)
+ response
+ end
+
+ context 'with `admin_mode` scope' do
+ let(:token) { create(:personal_access_token, user: admin, scopes: [:api, :admin_mode]) }
+
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ end
+
+ context 'when admin mode setting is enabled' do
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ end
+ end
+
+ context 'without `admin_mode` scope' do
+ let(:token) { create(:personal_access_token, user: admin, scopes: [:api]) }
+
+ context 'when admin mode setting is disabled', :do_not_mock_admin_mode_setting do
+ it { is_expected.to have_gitlab_http_status(:ok) }
+ end
+
+ context 'when admin mode setting is enabled' do
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+ end
+ end
end
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index 5aba7e096a7..c08ecae28e8 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -5,21 +5,15 @@ require 'spec_helper'
RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
+ let_it_be(:path) { "/application/appearance" }
describe "GET /application/appearance" do
- context 'as a non-admin user' do
- it "returns 403" do
- get api("/application/appearance", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'GET request permissions for admin mode'
context 'as an admin user' do
it "returns appearance" do
- get api("/application/appearance", admin)
+ get api("/application/appearance", admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['description']).to eq('')
expect(json_response['email_header_and_footer_enabled']).to be(false)
@@ -34,32 +28,29 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation 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['pwa_name']).to eq('')
expect(json_response['pwa_short_name']).to eq('')
+ expect(json_response['pwa_description']).to eq('')
end
end
end
describe "PUT /application/appearance" do
- context 'as a non-admin user' do
- it "returns 403" do
- put api("/application/appearance", user), params: { title: "Test" }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'PUT request permissions for admin mode', { title: "Test" }
context 'as an admin user' do
context "instance basics" do
it "allows updating the settings" do
- put api("/application/appearance", admin), params: {
+ put api("/application/appearance", admin, admin_mode: true), params: {
title: "GitLab Test Instance",
- pwa_short_name: "GitLab PWA",
description: "gitlab-test.example.com",
+ pwa_name: "GitLab PWA Test",
+ pwa_short_name: "GitLab PWA",
+ pwa_description: "This is GitLab as PWA",
new_project_guidelines: "Please read the FAQs for help.",
profile_image_guidelines: "Custom profile image guidelines"
}
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['description']).to eq('gitlab-test.example.com')
expect(json_response['email_header_and_footer_enabled']).to be(false)
@@ -74,7 +65,9 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation 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['pwa_name']).to eq('GitLab PWA Test')
expect(json_response['pwa_short_name']).to eq('GitLab PWA')
+ expect(json_response['pwa_description']).to eq('This is GitLab as PWA')
end
end
@@ -88,7 +81,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
email_header_and_footer_enabled: true
}
- put api("/application/appearance", admin), params: settings
+ put api("/application/appearance", admin, admin_mode: true), params: settings
expect(response).to have_gitlab_http_status(:ok)
settings.each do |attribute, value|
@@ -98,14 +91,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
context "fails on invalid color values" do
it "with message_font_color" do
- put api("/application/appearance", admin), params: { message_font_color: "No Color" }
+ put api("/application/appearance", admin, admin_mode: true), params: { message_font_color: "No Color" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['message_font_color']).to contain_exactly('must be a valid color code')
end
it "with message_background_color" do
- put api("/application/appearance", admin), params: { message_background_color: "#1" }
+ put api("/application/appearance", admin, admin_mode: true), params: { message_background_color: "#1" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['message_background_color']).to contain_exactly('must be a valid color code')
@@ -117,7 +110,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
let_it_be(:appearance) { create(:appearance) }
it "allows updating the image files" do
- put api("/application/appearance", admin), params: {
+ put api("/application/appearance", admin, admin_mode: true), params: {
logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
header_logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
pwa_icon: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
@@ -133,14 +126,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
context "fails on invalid color images" do
it "with string instead of file" do
- put api("/application/appearance", admin), params: { logo: 'not-a-file.png' }
+ put api("/application/appearance", admin, admin_mode: true), params: { logo: 'not-a-file.png' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("logo is invalid")
end
it "with .svg file instead of .png" do
- put api("/application/appearance", admin), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") }
+ put api("/application/appearance", admin, admin_mode: true), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['favicon']).to contain_exactly("You are not allowed to upload \"svg\" files, allowed types: png, ico")
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index e238a1fb554..b81cdcfea8e 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -3,21 +3,23 @@
require 'spec_helper'
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' }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:scopes) { 'api' }
+ let_it_be(:path) { "/applications" }
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) }
describe 'POST /applications' do
+ it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' }
+
context 'authenticated and authorized user' do
it 'creates and returns an OAuth application' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
end.to change { Doorkeeper::Application.count }.by 1
application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url')
- expect(response).to have_gitlab_http_status(:created)
expect(json_response).to be_a Hash
expect(json_response['application_id']).to eq application.uid
expect(json_response['secret']).to eq application.secret
@@ -28,7 +30,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application with the wrong redirect_uri format' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://', scopes: scopes }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -38,7 +40,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application with a forbidden URI format' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'javascript://alert()', scopes: scopes }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'javascript://alert()', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -48,7 +50,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application without a name' do
expect do
- post api('/applications', admin_user), params: { redirect_uri: 'http://application.url', scopes: scopes }
+ post api(path, admin, admin_mode: true), params: { redirect_uri: 'http://application.url', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -58,7 +60,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application without a redirect_uri' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', scopes: scopes }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -68,7 +70,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application without specifying `scopes`' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url' }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url' }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -78,7 +80,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application with blank `scopes`' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '' }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '' }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -87,7 +89,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application with invalid `scopes`' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'non_existent_scope' }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'non_existent_scope' }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -97,7 +99,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
context 'multiple scopes' do
it 'creates an application with multiple `scopes` when each scope specified is seperated by a space' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api read_user' }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api read_user' }
end.to change { Doorkeeper::Application.count }.by 1
application = Doorkeeper::Application.last
@@ -108,7 +110,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'does not allow creating an application with multiple `scopes` when one of the scopes is invalid' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api non_existent_scope' }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api non_existent_scope' }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -118,7 +120,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
it 'defaults to creating an application with confidential' do
expect do
- post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes, confidential: nil }
+ post api(path, admin, admin_mode: true), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes, confidential: nil }
end.to change { Doorkeeper::Application.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -133,15 +135,13 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
expect do
post api('/applications', user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
-
- expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'non-authenticated user' do
it 'does not create application' do
expect do
- post api('/applications'), params: { name: 'application_name', redirect_uri: 'http://application.url' }
+ post api(path), params: { name: 'application_name', redirect_uri: 'http://application.url' }
end.not_to change { Doorkeeper::Application.count }
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -150,26 +150,17 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
end
describe 'GET /applications' do
- context 'authenticated and authorized user' do
- it 'can list application' do
- get api('/applications', admin_user)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a(Array)
- end
- end
+ it_behaves_like 'GET request permissions for admin mode'
- context 'authorized user without authorization' do
- it 'cannot list application' do
- get api('/applications', user)
+ it 'can list application' do
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ expect(json_response).to be_a(Array)
end
context 'non-authenticated user' do
it 'cannot list application' do
- get api('/applications')
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -177,33 +168,29 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
end
describe 'DELETE /applications/:id' do
+ context 'user authorization' do
+ let!(:path) { "/applications/#{application.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+ end
+
context 'authenticated and authorized user' do
it 'can delete an application' do
expect do
- delete api("/applications/#{application.id}", admin_user)
+ delete api("#{path}/#{application.id}", admin, admin_mode: true)
end.to change { Doorkeeper::Application.count }.by(-1)
-
- expect(response).to have_gitlab_http_status(:no_content)
end
it 'cannot delete non-existing application' do
- delete api("/applications/#{non_existing_record_id}", admin_user)
+ delete api("#{path}/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user without authorization' do
- it 'cannot delete an application' do
- delete api("/applications/#{application.id}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'non-authenticated user' do
it 'cannot delete an application' do
- delete api("/applications/#{application.id}")
+ delete api("#{path}/#{application.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
index 8affbe6ec2b..fcef5b6ca78 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, feature_category: :users do
+RSpec.describe API::Avatar, feature_category: :user_profile do
let(:gravatar_service) { double('GravatarService') }
describe 'GET /avatar' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index eba1a06b5e4..058ddaebd79 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -279,7 +279,7 @@ RSpec.describe API::Branches, feature_category: :source_code_management do
expect do
get api(route, current_user), params: { per_page: 100 }
- end.not_to exceed_query_limit(control)
+ end.not_to exceed_query_limit(control).with_threshold(1)
end
end
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index 4fb4fbe6d5c..23dfe865ba3 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
before do
stub_application_setting(bulk_import_enabled: true)
+
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
shared_examples 'disabled feature' do
@@ -73,6 +75,24 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
describe 'POST /bulk_imports' do
+ let(:request) { post api('/bulk_imports', user), params: params }
+ let(:destination_param) { { destination_slug: 'destination_slug' } }
+ let(:params) do
+ {
+ configuration: {
+ url: 'http://gitlab.example',
+ access_token: 'access_token'
+ },
+ entities: [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full_path',
+ destination_namespace: 'destination_namespace'
+ }.merge(destination_param)
+ ]
+ }
+ end
+
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance)
@@ -86,23 +106,6 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
shared_examples 'starting a new migration' do
- let(:request) { post api('/bulk_imports', user), params: params }
- let(:params) do
- {
- configuration: {
- url: 'http://gitlab.example',
- access_token: 'access_token'
- },
- entities: [
- {
- source_type: 'group_entity',
- source_full_path: 'full_path',
- destination_namespace: 'destination_namespace'
- }.merge(destination_param)
- ]
- }
- end
-
it 'starts a new migration' do
request
@@ -278,6 +281,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
include_examples 'disabled feature'
+
+ context 'when request exceeds rate limits' do
+ it 'prevents user from starting a new migration' do
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+ end
end
describe 'GET /bulk_imports/entities' do
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index a4a38179d11..ee390773f29 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
+ include Ci::JobTokenScopeHelpers
include HttpIOHelpers
@@ -312,7 +313,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'normal authentication' do
context 'job with artifacts' do
context 'when artifacts are stored locally' do
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, project: project) }
subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) }
@@ -329,11 +330,12 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
stub_licensed_features(cross_project_pipelines: true)
end
- it_behaves_like 'downloads artifact'
-
context 'when job token scope is enabled' do
before do
- other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true)
+ other_job.project.ci_cd_settings.update!(
+ job_token_scope_enabled: true,
+ inbound_job_token_scope_enabled: true
+ )
end
it 'does not allow downloading artifacts' do
@@ -343,7 +345,9 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is added to the job token scope' do
- let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) }
+ before do
+ make_project_fully_accessible(other_job.project, job.project)
+ end
it_behaves_like 'downloads artifact'
end
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 875bfc5b94f..10dd9c3b556 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -126,6 +126,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
it 'returns specific job data' do
expect(json_response['finished_at']).to be_nil
+ expect(json_response['erased_at']).to be_nil
end
it 'avoids N+1 queries', :skip_before_request do
@@ -540,21 +541,6 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
expect(json_response.first['id']).to eq(job.id)
expect(response.headers).not_to include("Link")
end
-
- context 'with :jobs_api_keyset_pagination disabled' do
- before do
- stub_feature_flags(jobs_api_keyset_pagination: false)
- end
-
- it 'defaults to offset pagination' do
- get api("/projects/#{project.id}/jobs", api_user), params: { pagination: 'keyset', per_page: 1 }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(running_job.id)
- expect(response.headers["Link"]).not_to include("cursor")
- end
- end
end
describe 'GET /projects/:id/jobs rate limited' do
@@ -651,6 +637,18 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
end
+ context 'when job is erased' do
+ let(:job) do
+ create(:ci_build, pipeline: pipeline, erased_at: Time.now)
+ end
+
+ it 'returns specific job data' do
+ get api("/projects/#{project.id}/jobs/#{job.id}", api_user)
+
+ expect(Time.parse(json_response['erased_at'])).to be_like_time(job.erased_at)
+ end
+ end
+
context 'when trace artifact record exists with no stored file', :skip_before_request do
before do
create(:ci_job_artifact, :unarchived_trace_artifact, job: job, project: job.project)
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index 22817922b1b..ef3b38e3fc4 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -21,11 +21,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let_it_be(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
let_it_be(:user) { create(:user) }
describe 'PUT /api/v4/jobs/:id' do
let_it_be_with_reload(:job) do
- create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id)
+ create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user,
+ runner_id: runner.id, runner_machine: runner_machine)
end
before do
@@ -38,6 +40,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it 'updates runner info' do
expect { update_job(state: 'success') }.to change { runner.reload.contacted_at }
+ .and change { runner_machine.reload.contacted_at }
end
context 'when status is given' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index d15bc9d2dd5..6e721d40560 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, feature_category: :runner do
+RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :continuous_integration do
include StubGitlabCalls
include RedisHelpers
include WorkhorseHelpers
@@ -119,6 +119,63 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
+ context 'when system_id parameter is specified' do
+ subject(:request) { request_job(**args) }
+
+ context 'with create_runner_machine FF enabled' do
+ before do
+ stub_feature_flags(create_runner_machine: true)
+ end
+
+ context 'when ci_runner_machines with same system_xid does not exist' do
+ let(:args) { { system_id: 's_some_system_id' } }
+
+ it 'creates respective ci_runner_machines record', :freeze_time do
+ expect { request }.to change { runner.runner_machines.reload.count }.from(0).to(1)
+
+ machine = runner.runner_machines.last
+ expect(machine.system_xid).to eq args[:system_id]
+ expect(machine.runner).to eq runner
+ expect(machine.contacted_at).to eq Time.current
+ end
+ end
+
+ context 'when ci_runner_machines with same system_xid already exists', :freeze_time do
+ let(:args) { { system_id: 's_existing_system_id' } }
+ let!(:runner_machine) do
+ create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago)
+ end
+
+ it 'does not create new ci_runner_machines record' do
+ expect { request }.not_to change { Ci::RunnerMachine.count }
+ end
+
+ it 'updates the contacted_at field' do
+ request
+
+ expect(runner_machine.reload.contacted_at).to eq Time.current
+ end
+ end
+ end
+
+ context 'with create_runner_machine FF disabled' do
+ before do
+ stub_feature_flags(create_runner_machine: false)
+ end
+
+ context 'when ci_runner_machines with same system_xid does not exist' do
+ let(:args) { { system_id: 's_some_system_id' } }
+
+ it 'does not create respective ci_runner_machines record', :freeze_time, :aggregate_failures do
+ expect { request }.not_to change { runner.runner_machines.reload.count }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(runner.runner_machines).to be_empty
+ end
+ end
+ end
+ end
+
context 'when jobs are finished' do
before do
job.success
diff --git a/spec/requests/api/ci/runner/runners_reset_spec.rb b/spec/requests/api/ci/runner/runners_reset_spec.rb
index 6ab21138d26..2d1e366e820 100644
--- a/spec/requests/api/ci/runner/runners_reset_spec.rb
+++ b/spec/requests/api/ci/runner/runners_reset_spec.rb
@@ -34,9 +34,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect do
post api("/runners/reset_authentication_token"), params: { token: group_runner.reload.token }
+ group_runner.reload
expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq({ 'token' => group_runner.reload.token, 'token_expires_at' => group_runner.reload.token_expires_at.iso8601(3) })
- expect(group_runner.reload.token_expires_at).to eq(5.days.from_now)
+ expect(json_response).to eq({ 'token' => group_runner.token, 'token_expires_at' => group_runner.token_expires_at.iso8601(3) })
+ expect(group_runner.token_expires_at).to eq(5.days.from_now)
end.to change { group_runner.reload.token }
end
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 22a954cc444..a6a1ad947aa 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -18,7 +18,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe '/api/v4/runners' do
describe 'POST /api/v4/runners/verify' do
- let(:runner) { create(:ci_runner) }
+ let_it_be_with_reload(:runner) { create(:ci_runner, token_expires_at: 3.days.from_now) }
+
+ let(:params) {}
+
+ subject(:verify) { post api('/runners/verify'), params: params }
context 'when no token is provided' do
it 'returns 400 error' do
@@ -29,46 +33,116 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
context 'when invalid token is provided' do
+ let(:params) { { token: 'invalid-token' } }
+
it 'returns 403 error' do
- post api('/runners/verify'), params: { token: 'invalid-token' }
+ verify
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when valid token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
+
+ context 'with create_runner_machine FF enabled' do
+ before do
+ stub_feature_flags(create_runner_machine: true)
+ end
+
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ })
+ end
+
+ context 'with non-expiring runner token' do
+ before do
+ runner.update!(token_expires_at: nil)
+ end
+
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => nil
+ })
+ end
+ end
+
+ it_behaves_like 'storing arguments in the application context for the API' do
+ let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ end
+
+ context 'when system_id is provided' do
+ let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+
+ it 'creates a runner_machine' do
+ expect { verify }.to change { Ci::RunnerMachine.count }.by(1)
+ end
+ end
+ end
- it 'verifies Runner credentials' do
- subject
+ context 'with create_runner_machine FF disabled' do
+ before do
+ stub_feature_flags(create_runner_machine: false)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'verifies Runner credentials' do
+ verify
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ })
+ end
+
+ context 'when system_id is provided' do
+ let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+
+ it 'does not create a runner_machine', :aggregate_failures do
+ expect { verify }.not_to change { Ci::RunnerMachine.count }
- it_behaves_like 'storing arguments in the application context for the API' do
- let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
context 'when non-expired token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
it 'verifies Runner credentials' do
runner["token_expires_at"] = 10.days.from_now
runner.save!
- subject
+ verify
expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ })
end
end
context 'when expired token is provided' do
- subject { post api('/runners/verify'), params: { token: runner.token } }
+ let(:params) { { token: runner.token } }
it 'does not verify Runner credentials' do
runner["token_expires_at"] = 10.days.ago
runner.save!
- subject
+ verify
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index b07dd388390..ca051386265 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -794,7 +794,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
end
- context 'when runner is specific' do
+ context 'when runner is a project runner' do
it 'return jobs' do
get api("/runners/#{project_runner.id}/jobs", admin)
@@ -947,7 +947,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
end
- context 'when runner is specific' do
+ context 'when runner is a project runner' do
it 'return jobs' do
get api("/runners/#{project_runner.id}/jobs", user)
@@ -1203,7 +1203,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
- it 'enables specific runner' do
+ it 'enables project runner' do
expect do
post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id }
end.to change { project.runners.count }.by(+1)
@@ -1243,7 +1243,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when project runner is used' do
let!(:new_project_runner) { create(:ci_runner, :project) }
- it 'enables any specific runner' do
+ it 'enables any project runner' do
expect do
post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
end.to change { project.runners.count }.by(+1)
@@ -1255,7 +1255,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
- it 'does not enable specific runner' do
+ it 'does not enable project runner' do
expect do
post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
end.not_to change { project.runners.count }
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
index 700fd97152a..fc988800b56 100644
--- a/spec/requests/api/ci/secure_files_spec.rb
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -2,10 +2,9 @@
require 'spec_helper'
-RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
+RSpec.describe API::Ci::SecureFiles, feature_category: :mobile_devops do
before do
stub_ci_secure_file_object_storage
- stub_feature_flags(ci_secure_files: true)
stub_feature_flags(ci_secure_files_read_only: false)
end
@@ -128,6 +127,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq(secure_file.name)
expect(json_response['expires_at']).to be nil
expect(json_response['metadata']).to be nil
+ expect(json_response['file_extension']).to be nil
end
it 'returns project secure file details with metadata when supported' do
@@ -138,6 +138,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq(secure_file_with_metadata.name)
expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z')
expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at])
+ expect(json_response['file_extension']).to eq('cer')
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
@@ -250,6 +251,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq('upload-keystore.jks')
expect(json_response['checksum']).to eq(secure_file.checksum)
expect(json_response['checksum_algorithm']).to eq('sha256')
+ expect(json_response['file_extension']).to eq('jks')
secure_file = Ci::SecureFile.find(json_response['id'])
expect(secure_file.checksum).to eq(
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index c5d01afb7c4..0f9f1bc80d6 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -114,73 +114,92 @@ RSpec.describe API::Ci::Variables, feature_category: :pipeline_authoring do
describe 'POST /projects/:id/variables' do
context 'authorized user with proper permissions' do
- it 'creates variable' do
- expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
- end.to change { project.variables.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('PROTECTED_VALUE_2')
- expect(json_response['protected']).to be_truthy
- expect(json_response['masked']).to be_truthy
- expect(json_response['raw']).to be_truthy
- expect(json_response['variable_type']).to eq('env_var')
- end
+ context 'when the project is below the plan limit for variables' do
+ it 'creates variable' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
+ end.to change { project.variables.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('PROTECTED_VALUE_2')
+ expect(json_response['protected']).to be_truthy
+ expect(json_response['masked']).to be_truthy
+ expect(json_response['raw']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
+ end
- it 'masks the new value when logging' do
- masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+ it 'masks the new value when logging' do
+ masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
- expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
- post api("/projects/#{project.id}/variables", user),
- params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
- end
+ post api("/projects/#{project.id}/variables", user),
+ params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
+ end
- it 'creates variable with optional attributes' do
- expect do
- post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
- end.to change { project.variables.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('VALUE_2')
- expect(json_response['protected']).to be_falsey
- expect(json_response['masked']).to be_falsey
- expect(json_response['raw']).to be_falsey
- expect(json_response['variable_type']).to eq('file')
- end
+ it 'creates variable with optional attributes' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ end.to change { project.variables.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['protected']).to be_falsey
+ expect(json_response['masked']).to be_falsey
+ expect(json_response['raw']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
+ end
- it 'does not allow to duplicate variable key' do
- expect do
- post api("/projects/#{project.id}/variables", user), params: { key: variable.key, value: 'VALUE_2' }
- end.to change { project.variables.count }.by(0)
+ it 'does not allow to duplicate variable key' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { key: variable.key, value: 'VALUE_2' }
+ end.to change { project.variables.count }.by(0)
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
- it 'creates variable with a specific environment scope' do
- expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2', environment_scope: 'review/*' }
- end.to change { project.variables.reload.count }.by(1)
+ it 'creates variable with a specific environment scope' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2', environment_scope: 'review/*' }
+ end.to change { project.variables.reload.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['environment_scope']).to eq('review/*')
+ end
+
+ it 'allows duplicated variable key given different environment scopes' do
+ variable = create(:ci_variable, project: project)
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('VALUE_2')
- expect(json_response['environment_scope']).to eq('review/*')
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { key: variable.key, value: 'VALUE_2', environment_scope: 'review/*' }
+ end.to change { project.variables.reload.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq(variable.key)
+ expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['environment_scope']).to eq('review/*')
+ end
end
- it 'allows duplicated variable key given different environment scopes' do
- variable = create(:ci_variable, project: project)
+ context 'when the project is at the plan limit for variables' do
+ before do
+ create(:plan_limits, :default_plan, project_ci_variables: 1)
+ end
- expect do
- post api("/projects/#{project.id}/variables", user), params: { key: variable.key, value: 'VALUE_2', environment_scope: 'review/*' }
- end.to change { project.variables.reload.count }.by(1)
+ it 'returns a variable limit error' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), params: { key: 'TOO_MANY_VARS', value: 'too many' }
+ end.not_to change { project.variables.count }
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq(variable.key)
- expect(json_response['value']).to eq('VALUE_2')
- expect(json_response['environment_scope']).to eq('review/*')
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['base']).to contain_exactly(
+ 'Maximum number of project ci variables (1) exceeded'
+ )
+ end
end
end
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index f4d5ef3fe90..0c80b7d830f 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -36,6 +36,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
end
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ end
+
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
@@ -60,6 +66,12 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
end
+ describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ end
+
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 5258d26be17..46f79efd928 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -50,6 +50,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ end
+
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
@@ -78,6 +84,12 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
+ describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
+
+ it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ end
+
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
@@ -124,6 +136,35 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
+
+ context 'with codename and component' do
+ let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
+
+ it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
+ end
+
+ context 'with codename and without component' do
+ let(:extra_params) { { distribution: distribution.codename } }
+
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'Debian packages GET request', :bad_request, /component is missing/
+ end
+ end
+ end
+
+ context 'with a buildinfo' do
+ let(:file_name) { 'sample_1.2.3~alpha2_amd64.buildinfo' }
+
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like "Debian packages upload request", :created, nil
+
+ context 'with codename and component' do
+ let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
+
+ it_behaves_like "Debian packages upload request", :bad_request,
+ /^file_name Only debs and udebs can be directly added to a distribution$/
+ end
+ end
end
context 'with a changes file' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 38016375b8f..c5126dbd1c2 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -42,8 +42,7 @@ RSpec.describe API::Discussions, feature_category: :team_planning do
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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
context 'when fetching discussions' do
diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb
new file mode 100644
index 00000000000..e8f519e004d
--- /dev/null
+++ b/spec/requests/api/draft_notes_spec.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_2) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+
+ let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
+ let!(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) }
+ let!(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) }
+
+ let_it_be(:api_stub) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe "Get a list of merge request draft notes" do
+ it "returns 200 OK status" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "returns only draft notes authored by the current user" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+
+ draft_note_ids = json_response.pluck("id")
+
+ expect(draft_note_ids).to include(draft_note_by_current_user.id)
+ expect(draft_note_ids).not_to include(draft_note_by_random_user.id)
+ expect(draft_note_ids).not_to include(merge_request_note.id)
+ end
+ end
+
+ describe "Get a single draft note" do
+ context "when requesting an existing draft note by the user" do
+ before do
+ get api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ user
+ )
+ end
+
+ it "returns 200 OK status" do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "returns the requested draft note" do
+ expect(json_response["id"]).to eq(draft_note_by_current_user.id)
+ end
+
+ context "when requesting a non-existent draft note" do
+ it "returns a 404 Not Found response" do
+ get api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{DraftNote.last.id + 1}",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when requesting an existing draft note by another user" do
+ it "returns a 404 Not Found response" do
+ get api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ describe "delete a draft note" do
+ context "when deleting an existing draft note by the user" do
+ let!(:deleted_draft_note_id) { draft_note_by_current_user.id }
+
+ before do
+ delete api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ user
+ )
+ end
+
+ it "returns 204 No Content status" do
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it "deletes the specified draft note" do
+ expect(DraftNote.exists?(deleted_draft_note_id)).to eq(false)
+ end
+ end
+
+ context "when deleting a non-existent draft note" do
+ it "returns a 404 Not Found" do
+ delete api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{non_existing_record_id}",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when deleting a draft note by a different user" do
+ it "returns a 404 Not Found" do
+ delete api(
+ "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe "Publishing a draft note" do
+ let(:publish_draft_note) do
+ put api(
+ "#{api_stub}/draft_notes/#{draft_note_by_current_user.id}/publish",
+ user
+ )
+ end
+
+ context "when publishing an existing draft note by the user" do
+ it "returns 204 No Content status" do
+ publish_draft_note
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it "publishes the specified draft note" do
+ expect { publish_draft_note }.to change { Note.count }.by(1)
+ expect(DraftNote.exists?(draft_note_by_current_user.id)).to eq(false)
+ end
+ end
+
+ context "when publishing a non-existent draft note" do
+ it "returns a 404 Not Found" do
+ put api(
+ "#{api_stub}/draft_notes/#{non_existing_record_id}/publish",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when publishing a draft note by a different user" do
+ it "returns a 404 Not Found" do
+ put api(
+ "#{api_stub}/draft_notes/#{draft_note_by_random_user.id}/publish",
+ user
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when DraftNotes::PublishService returns a non-success" do
+ it "returns an :internal_server_error and a message" do
+ expect_next_instance_of(DraftNotes::PublishService) do |instance|
+ expect(instance).to receive(:execute).and_return({ status: :failure, message: "Error message" })
+ end
+
+ publish_draft_note
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 5c061a37ff3..f884aaabb53 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, feature_category: :users do
+RSpec.describe API::Events, feature_category: :user_profile 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/graphql/boards/board_list_query_spec.rb b/spec/requests/api/graphql/boards/board_list_query_spec.rb
index b5ed0fe35d5..6ddcf8a13fd 100644
--- a/spec/requests/api/graphql/boards/board_list_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_query_spec.rb
@@ -9,9 +9,11 @@ RSpec.describe 'Querying a Board list', feature_category: :team_planning do
let_it_be(:project) { create(:project) }
let_it_be(:board) { create(:board, resource_parent: project) }
let_it_be(:label) { create(:label, project: project, name: 'foo') }
+ let_it_be(:extra_label1) { create(:label, project: project) }
+ let_it_be(:extra_label2) { create(:label, project: project) }
let_it_be(:list) { create(:list, board: board, label: label) }
- let_it_be(:issue1) { create(:issue, project: project, labels: [label]) }
- let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) }
+ let_it_be(:issue1) { create(:issue, project: project, labels: [label, extra_label1]) }
+ let_it_be(:issue2) { create(:issue, project: project, labels: [label, extra_label2], assignees: [current_user]) }
let_it_be(:issue3) { create(:issue, project: project, labels: [label], confidential: true) }
let(:filters) { {} }
@@ -66,6 +68,18 @@ RSpec.describe 'Querying a Board list', feature_category: :team_planning do
is_expected.to include({ 'issuesCount' => 1, 'title' => list.title })
end
end
+
+ context 'when filtering by OR labels' do
+ let(:filters) { { or: { labelNames: [extra_label1.title, extra_label2.title] } } }
+
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it 'filters issues metadata' do
+ is_expected.to include({ 'issuesCount' => 2, 'title' => list.title })
+ end
+ end
end
end
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 0437a30eccd..95cabfea2fc 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -50,6 +50,7 @@ RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integratio
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
project.ci_cd_settings.inbound_job_token_scope_enabled?)
+ expect(settings_data['optInJwt']).to eql project.ci_cd_settings.opt_in_jwt?
end
end
end
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index e6d73701b8f..f76bb8ff837 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_categor
let_it_be(:user) { create(:user) }
let(:service) { Ci::ListConfigVariablesService.new(project, user) }
- let(:sha) { project.repository.commit.sha }
+ let(:ref) { project.default_branch }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
- ciConfigVariables(sha: "#{sha}") {
+ ciConfigVariables(sha: "#{ref}") {
key
value
valueOptions
@@ -47,7 +47,7 @@ RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_categor
it 'returns the CI variables for the config' do
expect(service)
.to receive(:execute)
- .with(sha)
+ .with(ref)
.and_call_original
post_graphql(query, current_user: user)
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index 51cbb4719f7..d78b30787c9 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_
post_graphql(query, current_user: user)
- expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(200)
+ expect(graphql_data.dig('group', 'ciVariables', 'limit')).to be(30000)
expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
@@ -72,4 +72,32 @@ RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_
expect(graphql_data.dig('group', 'ciVariables')).to be_nil
end
end
+
+ describe 'sorting and pagination' do
+ let_it_be(:current_user) { user }
+ let_it_be(:data_path) { [:group, :ci_variables] }
+ let_it_be(:variables) do
+ [
+ create(:ci_group_variable, group: group, key: 'd'),
+ create(:ci_group_variable, group: group, key: 'a'),
+ create(:ci_group_variable, group: group, key: 'c'),
+ create(:ci_group_variable, group: group, key: 'e'),
+ create(:ci_group_variable, group: group, key: 'b')
+ ]
+ end
+
+ def pagination_query(params)
+ graphql_query_for(
+ :group,
+ { fullPath: group.full_path },
+ query_graphql_field('ciVariables', params, "#{page_info} nodes { id }")
+ )
+ end
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'sorted paginated variables'
+ end
end
diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb
index d1588833d8f..1874e1d35dd 100644
--- a/spec/requests/api/graphql/ci/groups_spec.rb
+++ b/spec/requests/api/graphql/ci/groups_spec.rb
@@ -10,8 +10,9 @@ RSpec.describe 'Query.project.pipeline.stages.groups', feature_category: :contin
let(:group_graphql_data) { graphql_data_at(:project, :pipeline, :stages, :nodes, 0, :groups, :nodes) }
let_it_be(:ref) { 'master' }
- let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2', ref: ref) }
- let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1', ref: ref) }
+ let_it_be(:stage) { create(:ci_stage, pipeline: pipeline) }
+ let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2', ref: ref, ci_stage: stage) }
+ let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1', ref: ref, ci_stage: stage) }
let_it_be(:job_c) { create(:ci_bridge, pipeline: pipeline, name: 'spinach 0 1', ref: ref) }
let(:params) { {} }
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index e0397e17923..5b65ae88426 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -69,4 +69,28 @@ RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do
expect(graphql_data.dig('ciVariables')).to be_nil
end
end
+
+ describe 'sorting and pagination' do
+ let_it_be(:current_user) { create(:admin) }
+ let_it_be(:data_path) { [:ci_variables] }
+ let_it_be(:variables) do
+ [
+ create(:ci_instance_variable, key: 'd'),
+ create(:ci_instance_variable, key: 'a'),
+ create(:ci_instance_variable, key: 'c'),
+ create(:ci_instance_variable, key: 'e'),
+ create(:ci_instance_variable, key: 'b')
+ ]
+ end
+
+ def pagination_query(params)
+ graphql_query_for(
+ :ci_variables,
+ params,
+ "#{page_info} nodes { id }"
+ )
+ end
+
+ it_behaves_like 'sorted paginated variables'
+ end
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 131cdb77107..674407c0a0e 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
create(:ci_build_need, build: test_job, name: 'my test job')
end
- it 'reports the build needs and execution requirements', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347290' do
+ it 'reports the build needs and execution requirements' do
post_graphql(query, current_user: user)
expect(jobs_graphql_data).to contain_exactly(
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index 0338b58a0ea..0ddcac89b34 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipelin
post_graphql(query, current_user: user)
- expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(200)
+ expect(graphql_data.dig('project', 'ciVariables', 'limit')).to be(8000)
expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({
'id' => variable.to_global_id.to_s,
'key' => 'TEST_VAR',
@@ -66,4 +66,32 @@ RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipelin
expect(graphql_data.dig('project', 'ciVariables')).to be_nil
end
end
+
+ describe 'sorting and pagination' do
+ let_it_be(:current_user) { user }
+ let_it_be(:data_path) { [:project, :ci_variables] }
+ let_it_be(:variables) do
+ [
+ create(:ci_variable, project: project, key: 'd'),
+ create(:ci_variable, project: project, key: 'a'),
+ create(:ci_variable, project: project, key: 'c'),
+ create(:ci_variable, project: project, key: 'e'),
+ create(:ci_variable, project: project, key: 'b')
+ ]
+ end
+
+ def pagination_query(params)
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field('ciVariables', params, "#{page_info} nodes { id }")
+ )
+ end
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it_behaves_like 'sorted paginated variables'
+ end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index ca08e780758..986e3ce9e52 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -92,6 +92,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
run_untagged: runner.run_untagged,
ip_address: runner.ip_address,
runner_type: runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE',
+ ephemeral_authentication_token: nil,
executor_name: runner.executor_type&.dasherize,
architecture_name: runner.architecture,
platform_name: runner.platform,
@@ -518,6 +519,110 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
+ describe 'ephemeralAuthenticationToken', :freeze_time do
+ subject(:request) { post_graphql(query, current_user: user) }
+
+ let_it_be(:creator) { create(:user) }
+
+ let(:created_at) { Time.current }
+ let(:token_prefix) { registration_type == :authenticated_user ? 'glrt-' : '' }
+ let(:registration_type) {}
+ let(:query) do
+ %(
+ query {
+ runner(id: "#{runner.to_global_id}") {
+ id
+ ephemeralAuthenticationToken
+ }
+ }
+ )
+ end
+
+ let(:runner) do
+ create(:ci_runner, :group,
+ groups: [group], creator: creator, created_at: created_at,
+ registration_type: registration_type, token: "#{token_prefix}abc123")
+ end
+
+ before_all do
+ group.add_owner(creator) # Allow creating runners in the group
+ end
+
+ shared_examples 'an ephemeral_authentication_token' do
+ it 'returns token in ephemeral_authentication_token field' do
+ request
+
+ runner_data = graphql_data_at(:runner)
+ expect(runner_data).not_to be_nil
+ expect(runner_data).to match a_graphql_entity_for(runner, ephemeral_authentication_token: runner.token)
+ end
+ end
+
+ shared_examples 'a protected ephemeral_authentication_token' do
+ it 'returns nil ephemeral_authentication_token' do
+ request
+
+ runner_data = graphql_data_at(:runner)
+ expect(runner_data).not_to be_nil
+ expect(runner_data).to match a_graphql_entity_for(runner, ephemeral_authentication_token: nil)
+ end
+ end
+
+ context 'with request made by creator' do
+ let(:user) { creator }
+
+ context 'with runner created in UI' do
+ let(:registration_type) { :authenticated_user }
+
+ context 'with runner created in last 3 hours' do
+ let(:created_at) { (3.hours - 1.second).ago }
+
+ context 'with no runner machine registed yet' do
+ it_behaves_like 'an ephemeral_authentication_token'
+ end
+
+ context 'with first runner machine already registed' do
+ let!(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+
+ it_behaves_like 'a protected ephemeral_authentication_token'
+ end
+ end
+
+ context 'with runner created almost too long ago' do
+ let(:created_at) { (3.hours - 1.second).ago }
+
+ it_behaves_like 'an ephemeral_authentication_token'
+ end
+
+ context 'with runner created too long ago' do
+ let(:created_at) { 3.hours.ago }
+
+ it_behaves_like 'a protected ephemeral_authentication_token'
+ end
+ end
+
+ context 'with runner registered from command line' do
+ let(:registration_type) { :registration_token }
+
+ context 'with runner created in last 3 hours' do
+ let(:created_at) { (3.hours - 1.second).ago }
+
+ it_behaves_like 'a protected ephemeral_authentication_token'
+ end
+ end
+ end
+
+ context 'when request is made by non-creator of the runner' do
+ let(:user) { create(:admin) }
+
+ context 'with runner created in UI' do
+ let(:registration_type) { :authenticated_user }
+
+ it_behaves_like 'a protected ephemeral_authentication_token'
+ end
+ end
+ end
+
describe 'Query limits' do
def runner_query(runner)
<<~SINGLE
@@ -578,7 +683,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
QUERY
end
- it 'does not execute more queries per runner', :aggregate_failures do
+ it 'does not execute more queries per runner', :aggregate_failures, quarantine: "https://gitlab.com/gitlab-org/gitlab/-/issues/391442" 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 } }
@@ -647,6 +752,11 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
icon
text
}
+ project {
+ id
+ name
+ webUrl
+ }
shortSha
commitPath
finishedAt
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index 7937091ea7c..c5286b93251 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', feature_category: :not_owned do
+RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
@@ -223,4 +223,101 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :not_owned do
expect(parsed_id).to eq(project.to_global_id)
end
end
+
+ describe 'removal of deprecated items' do
+ let(:mock_schema) do
+ Class.new(GraphQL::Schema) do
+ lazy_resolve ::Gitlab::Graphql::Lazy, :force
+
+ query(Class.new(::Types::BaseObject) do
+ graphql_name 'Query'
+
+ field :foo, GraphQL::Types::Boolean,
+ deprecated: { milestone: '0.1', reason: :renamed }
+
+ field :bar, (Class.new(::Types::BaseEnum) do
+ graphql_name 'BarEnum'
+
+ value 'FOOBAR', value: 'foobar', deprecated: { milestone: '0.1', reason: :renamed }
+ end)
+
+ field :baz, GraphQL::Types::Boolean do
+ argument :arg, String, required: false, deprecated: { milestone: '0.1', reason: :renamed }
+ end
+
+ def foo
+ false
+ end
+
+ def bar
+ 'foobar'
+ end
+
+ def baz(arg:)
+ false
+ end
+ end)
+ end
+ end
+
+ let(:params) { {} }
+ let(:headers) { {} }
+
+ before do
+ allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args|
+ mock_schema.execute(*args)
+ end
+ end
+
+ context 'without `remove_deprecated` param' do
+ it 'shows deprecated items' do
+ query = '{ foo bar baz(arg: "test") }'
+
+ post_graphql(query, params: params, headers: headers)
+
+ expect(json_response).to include('data' => { 'foo' => false, 'bar' => 'FOOBAR', 'baz' => false })
+ end
+ end
+
+ context 'with `remove_deprecated` param' do
+ let(:params) { { remove_deprecated: '1' } }
+
+ it 'hides deprecated field' do
+ query = '{ foo }'
+
+ post_graphql(query, params: params)
+
+ expect(json_response).not_to include('data' => { 'foo' => false })
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /Field 'foo' doesn't exist on type 'Query'/))
+ )
+ end
+
+ it 'hides deprecated enum value' do
+ query = '{ bar }'
+
+ post_graphql(query, params: params)
+
+ expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
+ expect(json_response).to include(
+ 'errors' => include(
+ a_hash_including(
+ 'message' => /`Query.bar` returned `"foobar"` at `bar`, but this isn't a valid value for `BarEnum`/
+ )
+ )
+ )
+ end
+
+ it 'hides deprecated argument' do
+ query = '{ baz(arg: "test") }'
+
+ post_graphql(query, params: params)
+
+ expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /Field 'baz' doesn't accept argument 'arg'/))
+ )
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/group/group_releases_spec.rb b/spec/requests/api/graphql/group/group_releases_spec.rb
new file mode 100644
index 00000000000..931e7c19c18
--- /dev/null
+++ b/spec/requests/api/graphql/group/group_releases_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.group(fullPath).releases()', feature_category: :release_orchestration do
+ include GraphqlHelpers
+
+ include_context 'when releases and group releases shared context'
+
+ let(:resource_type) { :group }
+ let(:resource) { group }
+
+ describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do
+ context 'when the group is private' do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :repository, :private, group: group) }
+ let_it_be(:release) { create(:release, :with_evidence, project: project) }
+
+ before_all do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ end
+
+ context 'when the user is not logged in' do
+ let(:current_user) { stranger }
+
+ it_behaves_like 'no access to any release data'
+ end
+
+ context 'when the user has Guest permissions' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'no access to any repository-related fields'
+ end
+
+ context 'when the user has Reporter permissions' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'full access to all repository-related fields'
+ it_behaves_like 'no access to editUrl'
+ end
+
+ context 'when the user has Developer permissions' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'full access to all repository-related fields'
+ it_behaves_like 'access to editUrl'
+ end
+ end
+
+ context 'when the group is public' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :repository, :public, group: group) }
+ let_it_be(:release) { create(:release, :with_evidence, project: project) }
+
+ before_all do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ end
+
+ context 'when the user is not logged in' do
+ let(:current_user) { stranger }
+
+ it_behaves_like 'no access to any release data'
+ end
+
+ context 'when the user has Guest permissions' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'full access to all repository-related fields'
+ it_behaves_like 'no access to editUrl'
+ end
+
+ context 'when the user has Reporter permissions' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'full access to all repository-related fields'
+ it_behaves_like 'no access to editUrl'
+ end
+
+ context 'when the user has Developer permissions' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'full access to all repository-related fields'
+ it_behaves_like 'access to editUrl'
+ end
+ end
+ end
+
+ describe 'sorting and pagination' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let(:current_user) { developer }
+
+ let(:data_path) { [:group, :releases] }
+
+ before_all do
+ group.add_developer(developer)
+ end
+
+ def pagination_query(params)
+ graphql_query_for(
+ :group,
+ { full_path: group.full_path },
+ query_graphql_field(:releases, params, "#{page_info} nodes { tagName }")
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.pluck('tagName')
+ end
+
+ context 'when sorting by released_at' do
+ let_it_be(:release5) { create(:release, project: project, tag: 'v5.5.0', released_at: 3.days.from_now) }
+ let_it_be(:release1) { create(:release, project: project, tag: 'v5.1.0', released_at: 3.days.ago) }
+ let_it_be(:release4) { create(:release, project: project, tag: 'v5.4.0', released_at: 2.days.from_now) }
+ let_it_be(:release2) { create(:release, project: project, tag: 'v5.2.0', released_at: 2.days.ago) }
+ let_it_be(:release3) { create(:release, project: project, tag: 'v5.3.0', released_at: 1.day.ago) }
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :RELEASED_AT_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :RELEASED_AT_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/groups_query_spec.rb b/spec/requests/api/graphql/groups_query_spec.rb
new file mode 100644
index 00000000000..84c8d3c3388
--- /dev/null
+++ b/spec/requests/api/graphql/groups_query_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'searching groups', :with_license, feature_category: :subgroups do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:private_group) { create(:group, :private) }
+
+ let(:fields) do
+ <<~FIELDS
+ nodes {
+ #{all_graphql_fields_for('Group')}
+ }
+ FIELDS
+ end
+
+ let(:query) do
+ <<~QUERY
+ query {
+ groups {
+ #{fields}
+ }
+ }
+ QUERY
+ end
+
+ subject { post_graphql(query, current_user: user) }
+
+ describe "Query groups(search)" do
+ let(:groups) { graphql_data_at(:groups, :nodes) }
+ let(:names) { groups.map { |group| group["name"] } } # rubocop: disable Rails/Pluck
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+ end
+
+ it 'includes public groups' do
+ subject
+
+ expect(names).to eq([public_group.name])
+ end
+
+ it 'includes accessible private groups ordered by name' do
+ private_group.add_maintainer(user)
+
+ subject
+
+ expect(names).to eq([public_group.name, private_group.name])
+ end
+
+ context 'with `search` argument' do
+ let_it_be(:other_group) { create(:group, name: 'other-group') }
+
+ let(:query) do
+ <<~QUERY
+ query {
+ groups(search: "oth") {
+ #{fields}
+ }
+ }
+ QUERY
+ end
+
+ it 'filters groups by name' do
+ subject
+
+ expect(names).to contain_exactly(other_group.name)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 101de692aa5..3665fbc2df8 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -154,6 +154,47 @@ RSpec.describe 'Query.issue(id)', feature_category: :team_planning do
end
end
+ context 'when selecting `related_merge_requests`' do
+ let(:issue_fields) { ['relatedMergeRequests { nodes { id } }'] }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:mr_project) { project }
+ let!(:merge_request) do
+ attributes = {
+ author: user,
+ source_project: mr_project,
+ target_project: mr_project,
+ source_branch: 'master',
+ target_branch: 'test',
+ description: "See #{issue.to_reference}"
+ }
+
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: issue.project, noteable: issue,
+ author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ before do
+ project.add_developer(current_user)
+
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns the related merge request' do
+ expect(issue_data['relatedMergeRequests']['nodes']).to include a_hash_including({
+ 'id' => merge_request.to_global_id.to_s
+ })
+ end
+
+ context 'no permission to related merge request' do
+ let_it_be(:mr_project) { create(:project, :private) }
+
+ it 'does not return the related merge request' do
+ expect(issue_data['relatedMergeRequests']['nodes']).to be_empty
+ end
+ end
+ end
+
context 'when there is a confidential issue' do
let!(:confidential_issue) do
create(:issue, :confidential, project: project)
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index e67c92d6c33..e437e1bbcb0 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -109,18 +109,6 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
end
end
- context 'when the root_level_issues_query feature flag is disabled' do
- before do
- stub_feature_flags(root_level_issues_query: false)
- end
-
- it 'the field returns null' do
- post_graphql(query, current_user: developer)
-
- expect(graphql_data).to eq('issues' => nil)
- end
- end
-
context 'when no filters are provided' do
let(:all_query_params) { {} }
@@ -187,15 +175,21 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
end
context 'when fetching issues from multiple projects' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
post_query # warm-up
- control = ActiveRecord::QueryRecorder.new { post_query }
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_query }
+ expect_graphql_errors_to_be_empty
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)
+ private_group = create(:group, :private).tap { |group| group.add_developer(current_user) }
+ private_project = create(:project, :private, group: private_group)
+ create(:issue, project: private_project)
+
+ expect { post_query }.not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
end
end
diff --git a/spec/requests/api/graphql/mutations/achievements/create_spec.rb b/spec/requests/api/graphql/mutations/achievements/create_spec.rb
index 1713f050540..3082629d40f 100644
--- a/spec/requests/api/graphql/mutations/achievements/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/achievements/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Mutations::Achievements::Create, feature_category: :users do
+RSpec.describe Mutations::Achievements::Create, feature_category: :user_profile do
include GraphqlHelpers
include WorkhorseHelpers
@@ -13,15 +13,13 @@ RSpec.describe Mutations::Achievements::Create, feature_category: :users do
let(:mutation) { graphql_mutation(:achievements_create, params) }
let(:name) { 'Name' }
let(:description) { 'Description' }
- let(:revokeable) { false }
let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") }
let(:params) do
{
namespace_id: group.to_global_id,
name: name,
avatar: avatar,
- description: description,
- revokeable: revokeable
+ description: description
}
end
@@ -70,8 +68,7 @@ RSpec.describe Mutations::Achievements::Create, feature_category: :users do
expect(graphql_data_at(:achievements_create, :achievement)).to match a_hash_including(
'name' => name,
'namespace' => a_hash_including('id' => group.to_global_id.to_s),
- 'description' => description,
- 'revokeable' => revokeable
+ 'description' => description
)
end
end
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 490716ddbe2..55e728b2141 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
@@ -60,7 +60,7 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
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).allows?(target_project) }.from(false).to(true)
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
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 607c6bd85c2..f1296c054f9 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
@@ -5,7 +5,13 @@ require 'spec_helper'
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!) }
+ let_it_be(:project) do
+ create(:project,
+ ci_outbound_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ )
+ end
+
let_it_be(:target_project) { create(:project) }
let_it_be(:link) do
@@ -16,6 +22,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int
let(:variables) do
{
+ direction: 'OUTBOUND',
project_path: project.full_path,
target_project_path: target_project.full_path
}
@@ -61,12 +68,21 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int
target_project.add_guest(current_user)
end
- it 'removes the target project from the job token scope' do
+ it 'removes the target project from the job token outbound scope' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false)
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(-1)
+ end
+
+ it 'responds successfully' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(graphql_data_at(:ciJobTokenScopeRemoveProject, :ciJobTokenScope, :projects, :nodes))
+ .to contain_exactly({ 'path' => project.path })
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
index 0e43fa024f3..492c6946c99 100644
--- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'PipelineSchedulePlay', feature_category: :continuious_integration do
+RSpec.describe 'PipelineSchedulePlay', feature_category: :continuous_integration do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
@@ -42,14 +42,18 @@ RSpec.describe 'PipelineSchedulePlay', feature_category: :continuious_integratio
end
end
- context 'when authorized' do
+ context 'when authorized', :sidekiq_inline do
before do
project.add_maintainer(user)
pipeline_schedule.update_columns(next_run_at: 2.hours.ago)
end
context 'when mutation succeeds' do
+ let(:service_response) { instance_double('ServiceResponse', payload: new_pipeline) }
+ let(:new_pipeline) { instance_double('Ci::Pipeline', persisted?: true) }
+
it do
+ expect(Ci::CreatePipelineService).to receive_message_chain(:new, :execute).and_return(service_response)
post_graphql_mutation(mutation, current_user: user)
expect(mutation_response['pipelineSchedule']['id']).to include(pipeline_schedule.id.to_s)
@@ -61,14 +65,10 @@ RSpec.describe 'PipelineSchedulePlay', feature_category: :continuious_integratio
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)
+ .with(pipeline_schedule.id, user.id).and_return(nil)
post_graphql_mutation(mutation, current_user: user)
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
new file mode 100644
index 00000000000..c1da231a4a6
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+
+ let(:mutation) do
+ variables = {
+ id: pipeline_schedule.to_global_id.to_s,
+ **pipeline_schedule_parameters
+ }
+
+ graphql_mutation(
+ :pipeline_schedule_update,
+ variables,
+ <<-QL
+ pipelineSchedule {
+ id
+ description
+ cron
+ refForDisplay
+ active
+ cronTimezone
+ variables {
+ nodes {
+ key
+ value
+ }
+ }
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:pipeline_schedule_parameters) { {} }
+ let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_update) }
+
+ 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_developer(user)
+ end
+
+ context 'when success' do
+ let(:pipeline_schedule_parameters) do
+ {
+ description: 'updated_desc',
+ cron: '0 1 * * *',
+ cronTimezone: 'UTC',
+ ref: 'patch-x',
+ active: true,
+ variables: [
+ { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }
+ ]
+ }
+ end
+
+ it do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect_graphql_errors_to_be_empty
+
+ expect(mutation_response['pipelineSchedule']['id']).to eq(pipeline_schedule.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')
+ end
+ end
+
+ context 'when failure' do
+ context 'when params are invalid' do
+ let(:pipeline_schedule_parameters) do
+ {
+ description: '',
+ cron: 'abc',
+ cronTimezone: 'cCc',
+ ref: '',
+ 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",
+ "Ref can't be blank",
+ "Description can't be blank"
+ ]
+ )
+ end
+ end
+
+ context 'when params have duplicate variables' do
+ let(:pipeline_schedule_parameters) do
+ {
+ 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/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 7a6ee7c2ecc..99e55c44773 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
@@ -18,7 +18,8 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
full_path: project.full_path,
keep_latest_artifact: false,
job_token_scope_enabled: false,
- inbound_job_token_scope_enabled: false
+ inbound_job_token_scope_enabled: false,
+ opt_in_jwt: true
}
end
@@ -117,6 +118,15 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
end
end
+ it 'updates ci_opt_in_jwt' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_opt_in_jwt).to eq(true)
+ end
+
context 'when bad arguments are provided' do
let(:variables) { { full_path: '', keep_latest_artifact: false } }
diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
new file mode 100644
index 00000000000..b9c83311908
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Bulk update issues', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) }
+ let_it_be(:milestone) { create(:milestone, group: group) }
+
+ let(:parent) { project }
+ let(:max_issues) { Mutations::Issues::BulkUpdate::MAX_ISSUES }
+ let(:mutation) { graphql_mutation(:issues_bulk_update, base_arguments.merge(additional_arguments)) }
+ let(:mutation_response) { graphql_mutation_response(:issues_bulk_update) }
+ let(:current_user) { developer }
+ let(:base_arguments) { { parent_id: parent.to_gid.to_s, ids: updatable_issues.map { |i| i.to_gid.to_s } } }
+
+ let(:additional_arguments) do
+ {
+ assignee_ids: [current_user.to_gid.to_s],
+ milestone_id: milestone.to_gid.to_s
+ }
+ end
+
+ context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
+ before do
+ stub_feature_flags(bulk_update_issues_mutation: false)
+ end
+
+ it 'returns a resource not available error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => '`bulk_update_issues_mutation` feature flag is disabled.'
+ )
+ )
+ end
+ end
+
+ context 'when user can not update all issues' do
+ let_it_be(:forbidden_issue) { create(:issue) }
+
+ it 'updates only issues that the user can update' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ updatable_issues.each(&:reset)
+ forbidden_issue.reset
+ end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2).and(
+ not_change(forbidden_issue, :assignee_ids).from([])
+ )
+
+ expect(mutation_response).to include(
+ 'updatedIssueCount' => updatable_issues.count
+ )
+ end
+ end
+
+ context 'when user can update all issues' do
+ it 'updates all issues' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ updatable_issues.each(&:reload)
+ end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
+ .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+
+ expect(mutation_response).to include(
+ 'updatedIssueCount' => updatable_issues.count
+ )
+ end
+
+ context 'when current user cannot read the specified project' do
+ let_it_be(:parent) { create(:project, :private) }
+
+ it 'returns a resource not found error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => "The resource that you are attempting to access does not exist or you don't have " \
+ 'permission to perform this action'
+ )
+ )
+ end
+ end
+
+ context 'when scoping to a parent group' do
+ let(:parent) { group }
+
+ it 'updates all issues' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ updatable_issues.each(&:reload)
+ end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
+ .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+
+ expect(mutation_response).to include(
+ 'updatedIssueCount' => updatable_issues.count
+ )
+ end
+
+ context 'when current user cannot read the specified group' do
+ let(:parent) { create(:group, :private) }
+
+ it 'returns a resource not found error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => "The resource that you are attempting to access does not exist or you don't have " \
+ 'permission to perform this action'
+ )
+ )
+ end
+ end
+ end
+
+ context 'when setting arguments to null or none' do
+ let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
+
+ before do
+ updatable_issues.each do |issue|
+ issue.update!(assignees: [current_user], milestone: milestone)
+ end
+ end
+
+ it 'updates all issues' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ updatable_issues.each(&:reload)
+ end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([current_user.id] * 2).to([])
+ .and(change { updatable_issues.map(&:milestone_id) }.from([milestone.id] * 2).to([nil] * 2))
+
+ expect(mutation_response).to include(
+ 'updatedIssueCount' => updatable_issues.count
+ )
+ end
+ end
+ end
+
+ context 'when update service returns an error' do
+ before do
+ allow_next_instance_of(Issuable::BulkUpdateService) do |update_service|
+ allow(update_service).to receive(:execute).and_return(
+ ServiceResponse.error(message: 'update error', http_status: 422) # rubocop:disable Gitlab/ServiceResponse
+ )
+ end
+ end
+
+ it 'returns an error message' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_data.dig('issuesBulkUpdate', 'errors')).to contain_exactly('update error')
+ end
+ end
+
+ context 'when trying to update more than the max allowed' do
+ before do
+ stub_const('Mutations::Issues::BulkUpdate::MAX_ISSUES', updatable_issues.count - 1)
+ end
+
+ it "restricts updating more than #{Mutations::Issues::BulkUpdate::MAX_ISSUES} issues at the same time" do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' =>
+ format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: max_issues)
+ )
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
index 3907ebad9ce..1898ee5a62d 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
@@ -63,4 +63,20 @@ RSpec.describe 'Setting milestone of a merge request', feature_category: :code_r
expect(mutation_response['mergeRequest']['milestone']).to be_nil
end
end
+
+ context 'when passing an invalid milestone_id' do
+ let(:input) { { milestone_id: GitlabSchema.id_from_object(create(:milestone)).to_s } }
+
+ it 'does not set the milestone' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to include(
+ a_hash_including(
+ 'message' => "The resource that you are attempting to access does not exist " \
+ "or you don't have permission to perform this action"
+ )
+ )
+ end
+ end
end
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 00e25909746..a6253ba424b 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -122,8 +122,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning 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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes)
+ .update!(disabled: true)
end
it_behaves_like 'a Note mutation that does not create a Note'
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index eb45e2aa033..f40518a574b 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -57,8 +57,7 @@ RSpec.describe 'Destroying a Note', feature_category: :team_planning do
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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
it 'does not update the Note' do
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 dff8a87314b..7918bc860fe 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -50,8 +50,7 @@ RSpec.describe 'Updating a Note', feature_category: :team_planning do
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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
it 'does not update the Note' do
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 31d17401b9e..967ad75c906 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, feature_category: :users do
+RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profile do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index b33a394d023..ddd294e8f82 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -127,7 +127,9 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:fields) do
<<~FIELDS
workItem {
+ title
description
+ state
widgets {
type
... on WorkItemWidgetDescription {
@@ -179,6 +181,9 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
nodes { id }
}
}
+ ... on WorkItemWidgetDescription {
+ description
+ }
}
}
errors
@@ -201,6 +206,12 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:expected_labels) { [] }
it_behaves_like 'mutation updating work item labels'
+
+ context 'with quick action' do
+ let(:input) { { 'descriptionWidget' => { 'description' => "/remove_label ~\"#{existing_label.name}\"" } } }
+
+ it_behaves_like 'mutation updating work item labels'
+ end
end
context 'when only adding labels' do
@@ -208,6 +219,14 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:expected_labels) { [label1, label2, existing_label] }
it_behaves_like 'mutation updating work item labels'
+
+ context 'with quick action' do
+ let(:input) do
+ { 'descriptionWidget' => { 'description' => "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" } }
+ end
+
+ it_behaves_like 'mutation updating work item labels'
+ end
end
context 'when adding and removing labels' do
@@ -216,10 +235,47 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:expected_labels) { [label1, label2] }
it_behaves_like 'mutation updating work item labels'
+
+ context 'with quick action' do
+ let(:input) do
+ { 'descriptionWidget' => { 'description' =>
+ "/label ~\"#{label1.name}\" ~\"#{label2.name}\"\n/remove_label ~\"#{existing_label.name}\"" } }
+ end
+
+ it_behaves_like 'mutation updating work item labels'
+ end
+ end
+
+ context 'when the work item type does not support labels widget' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+
+ let(:input) { { 'descriptionWidget' => { 'description' => "Updating labels.\n/labels ~\"#{label1.name}\"" } } }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:labels).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change(work_item.labels, :count)
+
+ expect(work_item.labels).to be_empty
+ expect(mutation_response['workItem']['widgets']).to include(
+ 'description' => "Updating labels.",
+ 'type' => 'DESCRIPTION'
+ )
+ expect(mutation_response['workItem']['widgets']).not_to include(
+ 'labels',
+ 'type' => 'LABELS'
+ )
+ end
end
end
- context 'with due and start date widget input' do
+ context 'with due and start date widget input', :freeze_time do
let(:start_date) { Date.today }
let(:due_date) { 1.week.from_now.to_date }
let(:fields) do
@@ -231,6 +287,9 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
startDate
dueDate
}
+ ... on WorkItemWidgetDescription {
+ description
+ }
}
}
errors
@@ -259,6 +318,81 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
)
end
+ context 'when using quick action' do
+ let(:due_date) { Date.today }
+
+ context 'when removing due date' do
+ let(:input) { { 'descriptionWidget' => { 'description' => "/remove_due_date" } } }
+
+ before do
+ work_item.update!(due_date: due_date)
+ end
+
+ it 'updates start and due date' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :start_date).and(
+ change(work_item, :due_date).from(due_date).to(nil)
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include({
+ 'startDate' => nil,
+ 'dueDate' => nil,
+ 'type' => 'START_AND_DUE_DATE'
+ })
+ end
+ end
+
+ context 'when setting due date' do
+ let(:input) { { 'descriptionWidget' => { 'description' => "/due today" } } }
+
+ it 'updates due date' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :start_date).and(
+ change(work_item, :due_date).from(nil).to(due_date)
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include({
+ 'startDate' => nil,
+ 'dueDate' => Date.today.to_s,
+ 'type' => 'START_AND_DUE_DATE'
+ })
+ end
+ end
+
+ context 'when the work item type does not support start and due date widget' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+
+ let(:input) { { 'descriptionWidget' => { 'description' => "Updating due date.\n/due today" } } }
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:start_and_due_date).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change(work_item, :due_date)
+
+ expect(mutation_response['workItem']['widgets']).to include(
+ 'description' => "Updating due date.",
+ 'type' => 'DESCRIPTION'
+ )
+ expect(mutation_response['workItem']['widgets']).not_to include({
+ 'dueDate' => nil,
+ 'type' => 'START_AND_DUE_DATE'
+ })
+ end
+ end
+ end
+
context 'when provided input is invalid' do
let(:due_date) { 1.week.ago.to_date }
@@ -516,6 +650,9 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
}
}
}
+ ... on WorkItemWidgetDescription {
+ description
+ }
}
}
errors
@@ -544,6 +681,81 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
}
)
end
+
+ context 'when using quick action' do
+ context 'when assigning a user' do
+ let(:input) { { 'descriptionWidget' => { 'description' => "/assign @#{developer.username}" } } }
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :assignee_ids).from([]).to([developer.id])
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'ASSIGNEES',
+ 'assignees' => {
+ 'nodes' => [
+ { 'id' => developer.to_global_id.to_s, 'username' => developer.username }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context 'when unassigning a user' do
+ let(:input) { { 'descriptionWidget' => { 'description' => "/unassign @#{developer.username}" } } }
+
+ before do
+ work_item.update!(assignee_ids: [developer.id])
+ end
+
+ it 'updates the work item assignee' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :assignee_ids).from([developer.id]).to([])
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ 'type' => 'ASSIGNEES',
+ 'assignees' => {
+ 'nodes' => []
+ }
+ )
+ end
+ end
+ end
+
+ context 'when the work item type does not support the assignees widget' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+
+ let(:input) do
+ { 'descriptionWidget' => { 'description' => "Updating assignee.\n/assign @#{developer.username}" } }
+ end
+
+ before do
+ WorkItems::Type.default_by_type(:task).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'ignores the quick action' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change(work_item, :assignee_ids)
+
+ expect(mutation_response['workItem']['widgets']).to include({
+ 'description' => "Updating assignee.",
+ 'type' => 'DESCRIPTION'
+ }
+ )
+ expect(mutation_response['workItem']['widgets']).not_to include({ 'type' => 'ASSIGNEES' })
+ end
+ end
end
context 'when updating milestone' do
diff --git a/spec/requests/api/graphql/notes/note_spec.rb b/spec/requests/api/graphql/notes/note_spec.rb
new file mode 100644
index 00000000000..daceaec0b94
--- /dev/null
+++ b/spec/requests/api/graphql/notes/note_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.note(id)', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:note) { create(:note, noteable: issue, project: project) }
+ let_it_be(:system_note) { create(:note, :system, noteable: issue, project: project) }
+
+ let(:note_params) { { 'id' => global_id_of(note) } }
+ let(:note_data) { graphql_data['note'] }
+ let(:note_fields) { all_graphql_fields_for('Note'.classify) }
+
+ let(:query) do
+ graphql_query_for('note', note_params, note_fields)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ context 'when the user does not have access to read the note' do
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+
+ context 'when it is a system note' do
+ let(:note_params) { { 'id' => global_id_of(system_note) } }
+
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+ end
+ end
+
+ context 'when the user has access to read the note' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'returns note' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data['id']).to eq(global_id_of(note).to_s)
+ end
+
+ context 'when it is a system note' do
+ let(:note_params) { { 'id' => global_id_of(system_note) } }
+
+ it 'returns note' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data['id']).to eq(global_id_of(system_note).to_s)
+ end
+ end
+
+ context 'and notes widget is not available' do
+ before do
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:notes).update!(disabled: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+ end
+
+ context 'when note is internal' do
+ let_it_be(:note) { create(:note, :confidential, noteable: issue, project: project) }
+
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+
+ context 'and user can read confidential notes' do
+ let_it_be(:developer) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'returns note' do
+ post_graphql(query, current_user: developer)
+
+ expect(note_data['id']).to eq(global_id_of(note).to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
new file mode 100644
index 00000000000..1199aeb4c39
--- /dev/null
+++ b/spec/requests/api/graphql/notes/synthetic_note_resolver_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.synthetic_note(noteable_id, sha)', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:label_event, refind: true) do
+ create(:resource_label_event, user: current_user, issue: issue, label: label, action: 'add', created_at: 2.days.ago)
+ end
+
+ let(:label_note) { LabelNote.from_events([label_event]) }
+ let(:global_id) { ::Gitlab::GlobalId.build(label_note, model_name: LabelNote.to_s, id: label_note.discussion_id) }
+ let(:note_params) { { sha: label_note.discussion_id, noteable_id: global_id_of(issue) } }
+ let(:note_data) { graphql_data['syntheticNote'] }
+ let(:note_fields) { all_graphql_fields_for('Note'.classify) }
+
+ let(:query) do
+ graphql_query_for('synthetic_note', note_params, note_fields)
+ end
+
+ context 'when the user does not have access to read the note' do
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+ end
+
+ context 'when the user has access to read the note' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'returns synthetic note' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data['id']).to eq(global_id.to_s)
+ end
+
+ context 'and notes widget is not available' do
+ before do
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:notes).update!(disabled: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(note_data).to be nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 42927634119..82fcc5254ad 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -41,6 +41,7 @@ RSpec.describe 'package details', feature_category: :package_registry do
context 'with unauthorized user' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.add_guest(user)
end
it 'returns no packages' do
@@ -48,6 +49,47 @@ RSpec.describe 'package details', feature_category: :package_registry do
expect(graphql_data_at(:package)).to be_nil
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ it '`public_package` returns true' do
+ expect(graphql_data_at(:package, :public_package)).to eq(true)
+ end
+ end
+ end
+
+ context 'when project is public' do
+ let_it_be(:public_project) { create(:project, :public, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: public_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(package_details).to match_schema('graphql/packages/package_details')
+ end
+ end
+
+ it '`public_package` returns true' do
+ expect(graphql_data_at(:package, :public_package)).to eq(true)
+ end
end
context 'with authorized user' do
@@ -113,6 +155,29 @@ RSpec.describe 'package details', feature_category: :package_registry do
end
end
+ context 'versions field', :aggregate_failures do
+ let_it_be(:composer_package2) { create(:composer_package, project: project, name: composer_package.name) }
+ let_it_be(:composer_package3) { create(:composer_package, :error, project: project, name: composer_package.name) }
+ let_it_be(:pending_destruction) { create(:composer_package, :pending_destruction, project: project, name: composer_package.name) }
+
+ def run_query
+ versions_nodes = <<~QUERY
+ nodes { id }
+ QUERY
+
+ query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("versions", {}, versions_nodes))
+ post_graphql(query, current_user: user)
+ end
+
+ it 'returns other versions' do
+ run_query
+ versions_ids = graphql_data.dig('package', 'versions', 'nodes').pluck('id')
+ expected_ids = [composer_package2, composer_package3].map(&:to_gid).map(&:to_s)
+
+ expect(versions_ids).to contain_exactly(*expected_ids)
+ end
+ end
+
context 'pipelines field', :aggregate_failures do
let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
@@ -227,6 +292,49 @@ RSpec.describe 'package details', feature_category: :package_registry do
end
end
+ context 'public_package' do
+ context 'when project is private' do
+ let_it_be(:private_project) { create(:project, :private, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: private_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ private_project.add_developer(user)
+ end
+
+ it 'returns false' do
+ subject
+
+ expect(graphql_data_at(:package, :public_package)).to eq(false)
+ end
+
+ context 'with access to package registry for everyone' do
+ before do
+ private_project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ subject
+ end
+
+ it 'returns true' do
+ expect(graphql_data_at(:package, :public_package)).to eq(true)
+ end
+ end
+ end
+
+ context 'when project is public' do
+ let_it_be(:public_project) { create(:project, :public, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: public_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ subject
+ end
+
+ it 'returns true' do
+ expect(graphql_data_at(:package, :public_package)).to eq(true)
+ end
+ end
+ end
+
context 'with package that has no default status' do
before do
composer_package.update!(status: :error)
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 304edfbf4e4..55d223daf27 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_ma
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('AlertManagementAlert', excluded: ['assignees'])}
+ #{all_graphql_fields_for('AlertManagementAlert', excluded: %w[assignees relatedMergeRequests])}
}
QUERY
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 6aa96cfc070..76e5d687fd1 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -193,7 +193,8 @@ RSpec.describe 'getting merge request information nested in a project', feature_
'cherryPickOnCurrentMergeRequest' => false,
'revertOnCurrentMergeRequest' => false,
'updateMergeRequest' => false,
- 'canMerge' => false
+ 'canMerge' => false,
+ 'canApprove' => false
}
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
index d078659b954..444738cbc81 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', feature_category: :project_statistics do
+RSpec.describe 'rendering project statistics', feature_category: :shared do
include GraphqlHelpers
let(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index aa454349fcf..bc47f5a0248 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -5,226 +5,10 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).releases()', feature_category: :release_orchestration do
include GraphqlHelpers
- let_it_be(:stranger) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:developer) { create(:user) }
-
- let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
- let(:opened_url_params) { { state: 'opened', **base_url_params } }
- let(:merged_url_params) { { state: 'merged', **base_url_params } }
- let(:closed_url_params) { { state: 'closed', **base_url_params } }
-
- let(:query) do
- graphql_query_for(:project, { fullPath: project.full_path },
- %{
- releases {
- count
- nodes {
- tagName
- tagPath
- name
- commit {
- sha
- }
- assets {
- count
- sources {
- nodes {
- url
- }
- }
- }
- evidences {
- nodes {
- sha
- }
- }
- links {
- selfUrl
- openedMergeRequestsUrl
- mergedMergeRequestsUrl
- closedMergeRequestsUrl
- openedIssuesUrl
- closedIssuesUrl
- }
- }
- }
- })
- end
-
- let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
- let(:post_query) { post_graphql(query, current_user: current_user) }
-
- let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) }
-
- before do
- stub_default_url_options(host: 'www.example.com')
- end
-
- shared_examples 'correct total count' do
- let(:data) { graphql_data.dig('project', 'releases') }
-
- before do
- create_list(:release, 2, project: project)
-
- post_query
- end
-
- it 'returns the total count' do
- expect(data['count']).to eq(project.releases.count)
- end
- end
-
- shared_examples 'full access to all repository-related fields' do
- describe 'repository-related fields' do
- before do
- post_query
- end
-
- it 'returns data for fields that are protected in private projects' do
- expected_sources = release.sources.map do |s|
- { 'url' => s.url }
- end
-
- expected_evidences = release.evidences.map do |e|
- { 'sha' => e.sha }
- end
-
- expect(data).to eq(
- 'tagName' => release.tag,
- 'tagPath' => project_tag_path(project, release.tag),
- 'name' => release.name,
- 'commit' => {
- 'sha' => release.commit.sha
- },
- 'assets' => {
- 'count' => release.assets_count,
- 'sources' => {
- 'nodes' => expected_sources
- }
- },
- 'evidences' => {
- 'nodes' => expected_evidences
- },
- 'links' => {
- 'selfUrl' => project_release_url(project, release),
- 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
- 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
- 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
- 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
- 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
- }
- )
- end
- end
-
- it_behaves_like 'correct total count'
- end
-
- shared_examples 'no access to any repository-related fields' do
- describe 'repository-related fields' do
- before do
- post_query
- end
+ include_context 'when releases and group releases shared context'
- it 'does not return data for fields that expose repository information' do
- tag_name = release.tag
- release_name = release.name
- expect(data).to eq(
- 'tagName' => tag_name,
- 'tagPath' => nil,
- 'name' => release_name,
- 'commit' => nil,
- 'assets' => {
- 'count' => release.assets_count(except: [:sources]),
- 'sources' => {
- 'nodes' => []
- }
- },
- 'evidences' => {
- 'nodes' => []
- },
- 'links' => {
- 'closedIssuesUrl' => nil,
- 'closedMergeRequestsUrl' => nil,
- 'mergedMergeRequestsUrl' => nil,
- 'openedIssuesUrl' => nil,
- 'openedMergeRequestsUrl' => nil,
- 'selfUrl' => project_release_url(project, release)
- }
- )
- end
- end
-
- it_behaves_like 'correct total count'
- end
-
- # editUrl is tested separately becuase its permissions
- # are slightly different than other release fields
- shared_examples 'access to editUrl' do
- let(:query) do
- graphql_query_for(:project, { fullPath: project.full_path },
- %{
- releases {
- nodes {
- links {
- editUrl
- }
- }
- }
- })
- end
-
- before do
- post_query
- end
-
- it 'returns editUrl' do
- expect(data).to eq(
- 'links' => {
- 'editUrl' => edit_project_release_url(project, release)
- }
- )
- end
- end
-
- shared_examples 'no access to editUrl' do
- let(:query) do
- graphql_query_for(:project, { fullPath: project.full_path },
- %{
- releases {
- nodes {
- links {
- editUrl
- }
- }
- }
- })
- end
-
- before do
- post_query
- end
-
- it 'does not return editUrl' do
- expect(data).to eq(
- 'links' => {
- 'editUrl' => nil
- }
- )
- end
- end
-
- shared_examples 'no access to any release data' do
- before do
- post_query
- end
-
- it 'returns nil' do
- expect(data).to eq(nil)
- end
- end
+ let(:resource_type) { :project }
+ let(:resource) { project }
describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do
context 'when the project is private' do
@@ -312,7 +96,7 @@ RSpec.describe 'Query.project(fullPath).releases()', feature_category: :release_
def pagination_query(params)
graphql_query_for(
- :project,
+ resource_type,
{ full_path: sort_project.full_path },
query_graphql_field(:releases, params, "#{page_info} nodes { tagName }")
)
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index de35c943749..f49165a88ea 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
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(:reporter) { create(:user).tap { |reporter| project.add_reporter(reporter) } }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:milestone1) { create(:milestone, project: project) }
@@ -43,10 +44,10 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
shared_examples 'work items resolver without N + 1 queries' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
post_graphql(query, current_user: current_user) # warm-up
- control = ActiveRecord::QueryRecorder.new do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: current_user)
end
@@ -59,11 +60,12 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
last_edited_at: 1.week.ago,
project: project,
labels: [label1, label2],
- milestone: milestone2
+ milestone: milestone2,
+ author: reporter
)
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
expect_graphql_errors_to_be_empty
- expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
end
end
@@ -212,6 +214,19 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when filtering by author username' do
+ let_it_be(:author) { create(:author) }
+ let_it_be(:item_3) { create(:work_item, project: project, author: author) }
+
+ let(:item_filter_params) { { author_username: item_3.author.username } }
+
+ it 'returns correct results' do
+ post_graphql(query, current_user: current_user)
+
+ expect(item_ids).to match_array([item_3.to_global_id.to_s])
+ end
+ end
+
describe 'sorting and pagination' do
let(:data_path) { [:project, :work_items] }
diff --git a/spec/requests/api/graphql/subscriptions/notes/created_spec.rb b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb
new file mode 100644
index 00000000000..f955c14ef3b
--- /dev/null
+++ b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe 'Subscriptions::Notes::Created', feature_category: :team_planning do
+ include GraphqlHelpers
+ include Graphql::Subscriptions::Notes::Helper
+
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:task) { create(:work_item, :task, project: project) }
+
+ let(:current_user) { nil }
+ let(:subscribe) { notes_subscription('workItemNoteCreated', task, current_user) }
+ let(:response_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteCreated) }
+ let(:discussion) { graphql_dig_at(response_note, :discussion) }
+ let(:discussion_notes) { graphql_dig_at(discussion, :notes, :nodes) }
+
+ before do
+ stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema)
+ Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ end
+
+ subject(:response) do
+ subscription_response do
+ # this creates note defined with let lazily and triggers the subscription event
+ new_note
+ end
+ end
+
+ context 'when user is unauthorized' do
+ let(:new_note) { create(:note, noteable: task, project: project, type: 'DiscussionNote') }
+
+ it 'does not receive any data' do
+ expect(response).to be_nil
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:current_user) { guest }
+ let(:new_note) { create(:note, noteable: task, project: project, type: 'DiscussionNote') }
+
+ it 'receives created note' do
+ response
+ note = Note.find(new_note.id)
+
+ expect(response_note['id']).to eq(note.to_gid.to_s)
+ expect(discussion['id']).to eq(note.discussion.to_gid.to_s)
+ expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s])
+ end
+
+ context 'when a new note is created as a reply' do
+ let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') }
+
+ let(:new_note) do
+ create(:note, noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id)
+ end
+
+ it 'receives created note' do
+ response
+ reply = Note.find(new_note.id)
+
+ expect(response_note['id']).to eq(reply.to_gid.to_s)
+ expect(discussion['id']).to eq(reply.discussion.to_gid.to_s)
+ expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s, reply.to_gid.to_s])
+ end
+ end
+
+ context 'when note is confidential' do
+ let(:current_user) { reporter }
+ let(:new_note) { create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') }
+
+ context 'and user has permission to read confidential notes' do
+ it 'receives created note' do
+ response
+ confidential_note = Note.find(new_note.id)
+
+ expect(response_note['id']).to eq(confidential_note.to_gid.to_s)
+ expect(discussion['id']).to eq(confidential_note.discussion.to_gid.to_s)
+ expect(discussion_notes.pluck('id')).to eq([confidential_note.to_gid.to_s])
+ end
+
+ context 'and replying' do
+ let_it_be(:note, refind: true) do
+ create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote')
+ end
+
+ let(:new_note) do
+ create(:note, :confidential,
+ noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id)
+ end
+
+ it 'receives created note' do
+ response
+ reply = Note.find(new_note.id)
+
+ expect(response_note['id']).to eq(reply.to_gid.to_s)
+ expect(discussion['id']).to eq(reply.discussion.to_gid.to_s)
+ expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s, reply.to_gid.to_s])
+ end
+ end
+ end
+
+ context 'and user does not have permission to read confidential notes' do
+ let(:current_user) { guest }
+ let(:new_note) { create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') }
+
+ it 'does not receive note data' do
+ response
+ expect(response_note).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'when resource events are triggering note subscription' do
+ let_it_be(:label1) { create(:label, project: project, title: 'foo') }
+ let_it_be(:label2) { create(:label, project: project, title: 'bar') }
+
+ subject(:response) do
+ subscription_response do
+ # this creates note defined with let lazily and triggers the subscription event
+ resource_event
+ end
+ end
+
+ context 'when user is unauthorized' do
+ let(:resource_event) { create(:resource_label_event, issue: task, label: label1) }
+
+ it "does not receive discussion data" do
+ expect(response).to be_nil
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:current_user) { guest }
+ let(:resource_event) { create(:resource_label_event, issue: task, label: label1) }
+
+ it "receives created synthetic note as a discussion" do
+ response
+
+ event = ResourceLabelEvent.find(resource_event.id)
+ discussion_id = event.discussion_id
+ discussion_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'Discussion').to_s
+ note_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'LabelNote').to_s
+
+ expect(response_note['id']).to eq(note_gid)
+ expect(discussion['id']).to eq(discussion_gid)
+ expect(discussion_notes.size).to eq(1)
+ expect(discussion_notes.pluck('id')).to match_array([note_gid])
+ end
+
+ context 'when several label events are created' do
+ let(:resource_event) do
+ ResourceEvents::ChangeLabelsService.new(task, current_user).execute(added_labels: [label1, label2])
+ end
+
+ it "receives created synthetic note as a discussion" do
+ response
+
+ event = ResourceLabelEvent.where(label_id: [label1, label2]).first
+ discussion_id = event.discussion_id
+ discussion_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'Discussion').to_s
+ note_gid = ::Gitlab::GlobalId.as_global_id(discussion_id, model_name: 'LabelNote').to_s
+
+ expect(response_note['id']).to eq(note_gid)
+ expect(discussion['id']).to eq(discussion_gid)
+ expect(discussion_notes.size).to eq(1)
+ expect(discussion_notes.pluck('id')).to match_array([note_gid])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb b/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb
new file mode 100644
index 00000000000..d98f1cfe77e
--- /dev/null
+++ b/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe 'Subscriptions::Notes::Deleted', feature_category: :team_planning do
+ include GraphqlHelpers
+ include Graphql::Subscriptions::Notes::Helper
+
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:task) { create(:work_item, :task, project: project) }
+ let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') }
+ let_it_be(:reply_note, refind: true) do
+ create(:note, noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id)
+ end
+
+ let(:current_user) { nil }
+ let(:subscribe) { notes_subscription('workItemNoteDeleted', task, current_user) }
+ let(:deleted_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteDeleted) }
+
+ before do
+ stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema)
+ Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ end
+
+ subject(:response) do
+ subscription_response do
+ note.destroy!
+ end
+ end
+
+ context 'when user is unauthorized' do
+ it 'does not receive any data' do
+ expect(response).to be_nil
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:current_user) { guest }
+
+ it 'receives note id that is removed' do
+ expect(deleted_note['id']).to eq(note.to_gid.to_s)
+ expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s)
+ expect(deleted_note['lastDiscussionNote']).to be false
+ end
+
+ context 'when last discussion note is deleted' do
+ let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') }
+
+ it 'receives note id that is removed' do
+ expect(deleted_note['id']).to eq(note.to_gid.to_s)
+ expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s)
+ expect(deleted_note['lastDiscussionNote']).to be true
+ end
+ end
+
+ context 'when note is confidential' do
+ let_it_be(:note, refind: true) do
+ create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote')
+ end
+
+ it 'receives note id that is removed' do
+ expect(deleted_note['id']).to eq(note.to_gid.to_s)
+ expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s)
+ expect(deleted_note['lastDiscussionNote']).to be true
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb b/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb
new file mode 100644
index 00000000000..25c0a79e7aa
--- /dev/null
+++ b/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe 'Subscriptions::Notes::Updated', feature_category: :team_planning do
+ include GraphqlHelpers
+ include Graphql::Subscriptions::Notes::Helper
+
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:task) { create(:work_item, :task, project: project) }
+ let_it_be(:note, refind: true) { create(:note, noteable: task, project: task.project, type: 'DiscussionNote') }
+
+ let(:current_user) { nil }
+ let(:subscribe) { note_subscription('workItemNoteUpdated', task, current_user) }
+ let(:updated_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteUpdated) }
+
+ before do
+ stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema)
+ Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ end
+
+ subject(:response) do
+ subscription_response do
+ note.update!(note: 'changing the note body')
+ end
+ end
+
+ context 'when user is unauthorized' do
+ it 'does not receive any data' do
+ expect(response).to be_nil
+ end
+ end
+
+ context 'when user is authorized' do
+ let(:current_user) { reporter }
+
+ it 'receives updated note data' do
+ expect(updated_note['id']).to eq(note.to_gid.to_s)
+ expect(updated_note['body']).to eq('changing the note body')
+ end
+
+ context 'when note is confidential' do
+ let_it_be(:note, refind: true) do
+ create(:note, :confidential, noteable: task, project: task.project, type: 'DiscussionNote')
+ end
+
+ context 'and user has permission to read confidential notes' do
+ it 'receives updated note data' do
+ expect(updated_note['id']).to eq(note.to_gid.to_s)
+ expect(updated_note['body']).to eq('changing the note body')
+ end
+ end
+
+ context 'and user does not have permission to read confidential notes' do
+ let(:current_user) { guest }
+
+ it 'does not receive updated note data' do
+ expect(updated_note).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index 3e82d783a18..c19dfa6f3f3 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', feature_category: :users do
+RSpec.describe 'User', feature_category: :user_profile do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 6b5d437df83..0fad4f4ff3a 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -55,7 +55,12 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'title' => work_item.title,
'confidential' => work_item.confidential,
'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s),
- 'userPermissions' => { 'readWorkItem' => true, 'updateWorkItem' => true, 'deleteWorkItem' => false },
+ 'userPermissions' => {
+ 'readWorkItem' => true,
+ 'updateWorkItem' => true,
+ 'deleteWorkItem' => false,
+ 'adminWorkItem' => true
+ },
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
)
end
@@ -210,6 +215,20 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
it 'places the newest child item to the end of the children list' do
expect(hierarchy_children.last['id']).to eq(newest_child.to_gid.to_s)
end
+
+ context 'when relative position is set' do
+ let_it_be(:first_child) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
+
+ let_it_be(:first_link) do
+ create(:parent_link, work_item_parent: work_item, work_item: first_child, relative_position: 1)
+ end
+
+ it 'places children according to relative_position at the beginning of the children list' do
+ ordered_list = [first_child, oldest_child, child_item1, child_item2, newest_child]
+
+ expect(hierarchy_children.pluck('id')).to eq(ordered_list.map(&:to_gid).map(&:to_s))
+ end
+ end
end
end
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 90b9606ec7b..e3d538d72ba 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -88,51 +88,70 @@ RSpec.describe API::GroupVariables, feature_category: :pipeline_authoring do
context 'authorized user with proper permissions' do
let(:access_level) { :owner }
- it 'creates variable' do
- expect do
- post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
- end.to change { group.variables.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('PROTECTED_VALUE_2')
- expect(json_response['protected']).to be_truthy
- expect(json_response['masked']).to be_truthy
- expect(json_response['variable_type']).to eq('env_var')
- expect(json_response['environment_scope']).to eq('*')
- expect(json_response['raw']).to be_truthy
+ context 'when the group is below the plan limit for variables' do
+ it 'creates variable' do
+ expect do
+ post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
+ end.to change { group.variables.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('PROTECTED_VALUE_2')
+ expect(json_response['protected']).to be_truthy
+ expect(json_response['masked']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
+ expect(json_response['environment_scope']).to eq('*')
+ expect(json_response['raw']).to be_truthy
+ end
+
+ it 'masks the new value when logging' do
+ masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ post api("/groups/#{group.id}/variables", user),
+ params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
+ end
+
+ it 'creates variable with optional attributes' do
+ expect do
+ post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ end.to change { group.variables.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['protected']).to be_falsey
+ expect(json_response['masked']).to be_falsey
+ expect(json_response['raw']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
+ expect(json_response['environment_scope']).to eq('*')
+ end
+
+ it 'does not allow to duplicate variable key' do
+ expect do
+ post api("/groups/#{group.id}/variables", user), params: { key: variable.key, value: 'VALUE_2' }
+ end.to change { group.variables.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
- it 'masks the new value when logging' do
- masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
-
- expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
-
- post api("/groups/#{group.id}/variables", user),
- params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
- end
-
- it 'creates variable with optional attributes' do
- expect do
- post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
- end.to change { group.variables.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('VALUE_2')
- expect(json_response['protected']).to be_falsey
- expect(json_response['masked']).to be_falsey
- expect(json_response['raw']).to be_falsey
- expect(json_response['variable_type']).to eq('file')
- expect(json_response['environment_scope']).to eq('*')
- end
-
- it 'does not allow to duplicate variable key' do
- expect do
- post api("/groups/#{group.id}/variables", user), params: { key: variable.key, value: 'VALUE_2' }
- end.to change { group.variables.count }.by(0)
-
- expect(response).to have_gitlab_http_status(:bad_request)
+ context 'when the group is at the plan limit for variables' do
+ before do
+ create(:plan_limits, :default_plan, group_ci_variables: 1)
+ end
+
+ it 'returns a variable limit error' do
+ expect do
+ post api("/groups/#{group.id}/variables", user), params: { key: 'TOO_MANY_VARS', value: 'too many' }
+ end.not_to change { group.variables.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['base']).to contain_exactly(
+ 'Maximum number of group ci variables (1) exceeded'
+ )
+ end
end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 767f3e8b5b5..ca32271f573 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -651,6 +651,12 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
headers: gitlab_shell_internal_api_request_header
)
end
+
+ it "updates user's activity data" do
+ expect(::Users::ActivityService).to receive(:new).with(author: user, namespace: project.namespace, project: project)
+
+ request
+ end
end
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index dc631ad7921..be76e55269a 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -227,7 +227,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
context 'an agent is found' do
let_it_be(:agent_token) { create(:cluster_agent_token) }
- shared_examples 'agent token tracking'
+ include_examples 'agent token tracking'
context 'project is public' do
let(:project) { create(:project, :public) }
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index 9d3ab269ca1..bb0f557cfee 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, feature_category: :users do
+RSpec.describe API::Invitations, feature_category: :user_profile 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 93bf17d72d7..40d8f6d2395 100644
--- a/spec/requests/api/issue_links_spec.rb
+++ b/spec/requests/api/issue_links_spec.rb
@@ -138,6 +138,8 @@ RSpec.describe API::IssueLinks, feature_category: :team_planning do
params: { target_project_id: project.id, target_issue_iid: target_issue.iid, link_type: 'relates_to' }
expect_link_response(link_type: 'relates_to')
+ expect(json_response['source_issue']['id']).to eq(issue.id)
+ expect(json_response['target_issue']['id']).to eq(target_issue.id)
end
it 'returns 201 when sending full path of target project' do
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index b89db82b150..4b60eaadcbc 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -139,12 +139,6 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response).to be_an Array
end
- it_behaves_like 'issuable anonymous search' do
- let(:url) { '/issues' }
- let(:issuable) { issue }
- let(:result) { issuable.id }
- end
-
it_behaves_like 'issuable API rate-limited search' do
let(:url) { '/issues' }
let(:issuable) { issue }
@@ -274,31 +268,6 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:counts) { { all: 1, closed: 0, opened: 1 } }
it_behaves_like 'issues statistics'
-
- context 'with anonymous user' do
- let(:user) { nil }
-
- context 'with disable_anonymous_search disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it_behaves_like 'issues statistics'
- end
-
- context 'with disable_anonymous_search enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'returns a unprocessable entity 422' do
- get api("/issues_statistics"), params: params
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to include('User must be authenticated to use search')
- end
- end
- end
end
end
end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 7305da1305a..265091fa698 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -432,11 +432,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
}
end
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
+ context 'when allow_possible_spam application setting is false' do
it 'does not create a new project issue' do
expect { post_issue }.not_to change(Issue, :count)
end
@@ -454,7 +450,11 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it 'does creates a new project issue' do
expect { post_issue }.to change(Issue, :count).by(1)
end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index 2d7439d65c1..f0d174c9e78 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -204,11 +204,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
+ context 'when allow_possible_spam application setting is false' do
it 'does not update a project issue' do
expect { update_issue }.not_to change { issue.reload.title }
end
@@ -226,7 +222,11 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- context 'when allow_possible_spam feature flag is true' do
+ context 'when allow_possible_spam application setting is true' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
it 'updates a project issue' do
expect { update_issue }.to change { issue.reload.title }
end
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 092eb442f1f..20aa660d95b 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -125,6 +125,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url)
subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4cd93603c31..19a630e5218 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -50,12 +50,6 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
expect_successful_response_with_paginated_array
end
- it_behaves_like 'issuable anonymous search' do
- let(:url) { endpoint_path }
- let(:issuable) { merge_request }
- let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] }
- end
-
it_behaves_like 'issuable API rate-limited search' do
let(:url) { endpoint_path }
let(:issuable) { merge_request }
@@ -662,12 +656,6 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
)
end
- it_behaves_like 'issuable anonymous search' do
- let(:url) { '/merge_requests' }
- let(:issuable) { merge_request }
- let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] }
- end
-
it_behaves_like 'issuable API rate-limited search' do
let(:url) { '/merge_requests' }
let(:issuable) { merge_request }
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 30616964371..44574caf54a 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -263,6 +263,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
describe 'GET /namespaces/:namespace/exists' do
let_it_be(:namespace1) { create(:group, name: 'Namespace 1', path: 'namespace-1') }
let_it_be(:namespace2) { create(:group, name: 'Namespace 2', path: 'namespace-2') }
+ let_it_be(:namespace_with_dot) { create(:group, name: 'With Dot', path: 'with.dot') }
let_it_be(:namespace1sub) { create(:group, name: 'Sub Namespace 1', path: 'sub-namespace-1', parent: namespace1) }
let_it_be(:namespace2sub) { create(:group, name: 'Sub Namespace 2', path: 'sub-namespace-2', parent: namespace2) }
@@ -301,6 +302,14 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
expect(response.body).to eq(expected_json)
end
+ it 'supports dot in namespace path' do
+ get api("/namespaces/#{namespace_with_dot.path}/exists", user)
+
+ expected_json = { exists: true, suggests: ["#{namespace_with_dot.path}1"] }.to_json
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq(expected_json)
+ end
+
it 'returns JSON indicating the namespace does not exist without a suggestion' do
get api("/namespaces/non-existing-namespace/exists", user)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index c2d9db1e6fb..c0276e02eb7 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -210,8 +210,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
it 'does not fetch notes' do
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index cc399d25429..60406f380a5 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -90,7 +90,6 @@ ci_cd_settings:
unexposed_attributes:
- id
- project_id
- - group_runners_enabled
- merge_trains_enabled
- merge_pipelines_enabled
- auto_rollback_enabled
@@ -115,6 +114,7 @@ build_import_state: # import_state
- last_update_at
- last_successful_update_at
- correlation_id_value
+ - checksums
remapped_attributes:
status: import_status
last_error: import_error
@@ -161,6 +161,9 @@ project_setting:
- jitsu_key
- mirror_branch_regex
- allow_pipeline_trigger_approve_deployment
+ - emails_enabled
+ - pages_unique_domain_enabled
+ - pages_unique_domain
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
@@ -168,5 +171,13 @@ build_service_desk_setting: # service_desk_setting
- issue_template_key
- file_template_project_id
- outgoing_name
+ - custom_email_enabled
+ - custom_email
+ - custom_email_smtp_address
+ - custom_email_smtp_port
+ - custom_email_smtp_username
+ - encrypted_custom_email_smtp_password
+ - encrypted_custom_email_smtp_password_iv
+ - custom_email_smtp_password
remapped_attributes:
project_key: service_desk_address
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index 69d8eb76cf3..f904cd8fd6c 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, feature_category: :users do
+RSpec.describe API::ProjectEvents, feature_category: :user_profile 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_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index d3adef85f8d..c003ae9cd48 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(url, job_token: job.token) }
@@ -130,7 +130,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(url, job_token: job.token) }
@@ -229,8 +229,8 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
get api(package_url, user)
end
- pipeline = create(:ci_pipeline, user: user)
- create(:ci_build, user: user, pipeline: pipeline)
+ pipeline = create(:ci_pipeline, user: user, project: project)
+ create(:ci_build, user: user, pipeline: pipeline, project: project)
create(:package_build_info, package: package1, pipeline: pipeline)
expect do
@@ -262,7 +262,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
it_behaves_like 'no destroy url'
context 'with JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(package_url, job_token: job.token) }
@@ -324,7 +324,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
subject { get api(package_url, job_token: job.token) }
@@ -430,7 +430,7 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do
end
context 'with JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
it 'returns 403 for a user without enough permissions' do
project.add_developer(user)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 568486deb7f..267557b8137 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -256,7 +256,6 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
- stub_feature_flags(allow_possible_spam: false)
project.add_developer(user)
end
@@ -312,8 +311,6 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
-
- stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index d62f8a32453..e78ef2f7630 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', feature_category: :projects do
+RSpec.shared_examples 'languages and percentages JSON response' do
let(:expected_languages) { project.repository.languages.to_h { |language| language.values_at(:label, :value) } }
before do
@@ -46,7 +46,7 @@ RSpec.shared_examples 'languages and percentages JSON response', feature_categor
end
end
-RSpec.describe API::Projects do
+RSpec.describe API::Projects, feature_category: :projects do
include ProjectForksHelper
include WorkhorseHelpers
include StubRequests
@@ -207,7 +207,7 @@ RSpec.describe API::Projects do
let(:current_user) { user }
end
- shared_examples 'includes container_registry_access_level', :aggregate_failures do
+ shared_examples 'includes container_registry_access_level' do
it do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
@@ -2227,6 +2227,89 @@ RSpec.describe API::Projects do
end
end
+ describe 'GET /project/:id/share_locations' do
+ let_it_be(:root_group) { create(:group, :public, name: 'root group') }
+ let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1') }
+ let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2') }
+ let_it_be(:project) { create(:project, :private, group: project_group1) }
+
+ shared_examples_for 'successful groups response' do
+ it 'returns an array of groups' do
+ request
+
+ aggregate_failures 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.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
+ end
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'does not return the groups for the given project' do
+ get api("/projects/#{project.id}/share_locations")
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when authenticated' do
+ context 'when user is not the owner of the project' do
+ it 'does not return the groups' do
+ get api("/projects/#{project.id}/share_locations", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is the owner of the project' do
+ let(:request) { get api("/projects/#{project.id}/share_locations", user), params: params }
+ let(:params) { {} }
+
+ before do
+ project.add_owner(user)
+ project_group1.add_developer(user)
+ project_group2.add_developer(user)
+ end
+
+ context 'with default search' do
+ it_behaves_like 'successful groups response' do
+ let(:expected_groups) { [project_group1, project_group2] }
+ end
+ end
+
+ context 'when searching by group name' do
+ let(:params) { { search: 'group1' } }
+
+ it_behaves_like 'successful groups response' do
+ let(:expected_groups) { [project_group1] }
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as admin' do
+ let(:request) { get api("/projects/#{project.id}/share_locations", admin), params: {} }
+
+ context 'without share_with_group_lock' do
+ it_behaves_like 'successful groups response' do
+ let(:expected_groups) { [root_group, project_group1, project_group2] }
+ end
+ end
+
+ context 'with share_with_group_lock' do
+ before do
+ project.namespace.update!(share_with_group_lock: true)
+ end
+
+ it_behaves_like 'successful groups response' do
+ let(:expected_groups) { [] }
+ end
+ end
+ end
+ end
+
describe 'GET /projects/:id' do
context 'when unauthenticated' do
it 'does not return private projects' do
@@ -2297,7 +2380,7 @@ RSpec.describe API::Projects do
let(:project_attributes) { YAML.load_file(project_attributes_file) }
let(:expected_keys) do
- keys = project_attributes.map do |relation, relation_config|
+ keys = project_attributes.flat_map do |relation, relation_config|
begin
actual_keys = project.send(relation).attributes.keys
rescue NoMethodError
@@ -2307,7 +2390,7 @@ RSpec.describe API::Projects do
remapped_attributes = relation_config['remapped_attributes'] || {}
computed_attributes = relation_config['computed_attributes'] || []
actual_keys - unexposed_attributes - remapped_attributes.keys + remapped_attributes.values + computed_attributes
- end.flatten
+ end
unless Gitlab.ee?
keys -= %w[
@@ -2359,6 +2442,7 @@ RSpec.describe API::Projects do
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['group_runners_enabled']).to be_present
expect(json_response['creator_id']).to be_present
expect(json_response['namespace']).to be_present
expect(json_response['avatar_url']).to be_nil
@@ -2463,6 +2547,7 @@ RSpec.describe API::Projects do
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['group_runners_enabled']).to be_present
expect(json_response['creator_id']).to be_present
expect(json_response['namespace']).to be_present
expect(json_response['import_status']).to be_present
@@ -3662,8 +3747,8 @@ RSpec.describe API::Projects do
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
- '-/system/project/avatar/'\
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
+ '-/system/project/avatar/' \
"#{project3.id}/banana_sample.gif")
end
end
@@ -3678,8 +3763,8 @@ RSpec.describe API::Projects do
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
- '-/system/project/avatar/'\
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
+ '-/system/project/avatar/' \
"#{project_with_avatar.id}/rails_sample.png")
end
end
@@ -3695,8 +3780,8 @@ RSpec.describe API::Projects do
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq('changed description')
- expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\
- '-/system/project/avatar/'\
+ expect(json_response['avatar_url']).to eq('http://localhost/uploads/' \
+ '-/system/project/avatar/' \
"#{project_with_avatar.id}/banana_sample.gif")
end
end
@@ -4634,25 +4719,66 @@ RSpec.describe API::Projects do
describe 'POST /projects/:id/housekeeping' do
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
+ let(:params) { {} }
+
+ subject { post api("/projects/#{project.id}/housekeeping", user), params: params }
before do
- allow(Repositories::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
+ allow(Repositories::HousekeepingService).to receive(:new).with(project, :eager).and_return(housekeeping)
end
context 'when authenticated as owner' do
it 'starts the housekeeping process' do
expect(housekeeping).to receive(:execute).once
- post api("/projects/#{project.id}/housekeeping", user)
+ subject
expect(response).to have_gitlab_http_status(:created)
end
+ it 'logs an audit event' do
+ expect(housekeeping).to receive(:execute).once.and_yield
+ expect(::Gitlab::Audit::Auditor).to receive(:audit).with(a_hash_including(
+ name: 'manually_trigger_housekeeping',
+ author: user,
+ scope: project,
+ target: project,
+ message: "Housekeeping task: eager"
+ ))
+
+ subject
+ end
+
+ context 'when requesting prune' do
+ let(:params) { { task: :prune } }
+
+ it 'triggers a prune' do
+ expect(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping)
+ expect(housekeeping).to receive(:execute).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when requesting an unsupported task' do
+ let(:params) { { task: :unsupported_task } }
+
+ it 'responds with bad_request' do
+ expect(Repositories::HousekeepingService).not_to receive(:new)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when housekeeping lease is taken' do
it 'returns conflict' do
expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken)
- post api("/projects/#{project.id}/housekeeping", user)
+ subject
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/)
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 4a7821fcb0a..462cc1e3b5d 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::Release::Links, feature_category: :release_orchestration do
+ include Ci::JobTokenScopeHelpers
+
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:developer) { create(:user) }
@@ -51,7 +53,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: maintainer) }
+ let(:job) { create(:ci_build, :running, user: maintainer, project: project) }
it 'returns releases links' do
get api("/projects/#{project.id}/releases/v0.1/assets/links", job_token: job.token)
@@ -127,7 +129,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: maintainer) }
+ let(:job) { create(:ci_build, :running, user: maintainer, project: project) }
it 'returns releases link' do
get api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", job_token: job.token)
@@ -241,7 +243,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: maintainer) }
+ let(:job) { create(:ci_build, :running, user: maintainer, project: project) }
it 'creates a new release link' do
expect do
@@ -385,7 +387,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: maintainer) }
+ let(:job) { create(:ci_build, :running, user: maintainer, project: project) }
it 'updates the release link' do
put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}"), params: params.merge(job_token: job.token)
@@ -496,7 +498,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, :running, user: maintainer) }
+ let(:job) { create(:ci_build, :running, user: maintainer, project: project) }
it 'deletes the release link' do
expect do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index e209ad2b2d5..c3f99872cef 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -1215,11 +1215,23 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
context 'with a project milestone' do
- let(:milestone_params) { { milestones: [milestone.title] } }
+ shared_examples 'adds milestone' do
+ it 'adds the milestone' do
+ expect(response).to have_gitlab_http_status(:created)
+ expect(returned_milestones).to match_array(['v1.0'])
+ end
+ end
- it 'adds the milestone' do
- expect(response).to have_gitlab_http_status(:created)
- expect(returned_milestones).to match_array(['v1.0'])
+ context 'by title' do
+ let(:milestone_params) { { milestones: [milestone.title] } }
+
+ it_behaves_like 'adds milestone'
+ end
+
+ context 'by id' do
+ let(:milestone_params) { { milestone_ids: [milestone.id] } }
+
+ it_behaves_like 'adds milestone'
end
end
@@ -1408,18 +1420,14 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
context 'when a milestone is passed in' do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:milestone_title) { milestone.title }
- let(:params) { { milestones: [milestone_title] } }
+ let!(:milestone2) { create(:milestone, project: project, title: 'v2.0') }
before do
release.milestones << milestone
end
- context 'a different milestone' do
- let(:milestone_title) { 'v2.0' }
- let!(:milestone2) { create(:milestone, project: project, title: milestone_title) }
-
- it 'replaces the milestone' do
+ shared_examples 'updates milestone' do
+ it 'updates the milestone' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1427,8 +1435,20 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
end
+ context 'by title' do
+ let(:params) { { milestones: [milestone2.title] } }
+
+ it_behaves_like 'updates milestone'
+ end
+
+ context 'by id' do
+ let(:params) { { milestone_ids: [milestone2.id] } }
+
+ it_behaves_like 'updates milestone'
+ end
+
context 'an identical milestone' do
- let(:milestone_title) { 'v1.0' }
+ let(:params) { { milestones: [milestone.title] } }
it 'does not change the milestone' do
subject
@@ -1439,7 +1459,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
context 'an empty milestone' do
- let(:milestone_title) { nil }
+ let(:params) { { milestones: [] } }
it 'removes the milestone' do
subject
@@ -1476,13 +1496,26 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
context 'with all new' do
let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') }
let!(:milestone3) { create(:milestone, project: project, title: 'milestone3') }
- let(:params) { { milestones: [milestone2.title, milestone3.title] } }
- it 'replaces the milestones' do
- subject
+ shared_examples 'update milestones' do
+ it 'replaces the milestones' do
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(returned_milestones).to match_array(%w(milestone2 milestone3))
+ end
+ end
+
+ context 'by title' do
+ let(:params) { { milestones: [milestone2.title, milestone3.title] } }
+
+ it_behaves_like 'update milestones'
+ end
+
+ context 'by id' do
+ let(:params) { { milestone_ids: [milestone2.id, milestone3.id] } }
+
+ it_behaves_like 'update milestones'
end
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index dd0da0cb887..2bc4c177bc9 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -340,7 +340,6 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
- stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
@@ -406,7 +405,6 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
end
- stub_feature_flags(allow_possible_spam: false)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb
index 53f366371e5..ef9735fd8b0 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, feature_category: :users do
+RSpec.describe API::Users, feature_category: :user_profile 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 c063187fdf4..34867b13db2 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, feature_category: :users do
+RSpec.describe API::Users, feature_category: :user_profile do
include WorkhorseHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 66337b94c75..02b99eba8ce 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
include TermsHelper
include GitHttpHelpers
include WorkhorseHelpers
+ include Ci::JobTokenScopeHelpers
shared_examples 'pulls require Basic HTTP Authentication' do
context "when no credentials are provided" do
@@ -869,14 +870,15 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
- let(:build) { create(:ci_build, :running) }
- let(:other_project) { create(:project, :repository) }
-
- before do
- build.update!(project: project) # can't associate it on factory create
+ let(:build) { create(:ci_build, :running, project: project, user: user) }
+ let(:other_project) do
+ create(:project, :repository).tap do |o|
+ make_project_fully_accessible(project, o)
+ end
end
context 'when build created by system is authenticated' do
+ let(:user) { nil }
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
@@ -899,12 +901,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context 'and build created by' do
before do
- build.update!(user: user)
project.add_reporter(user)
- create(:ci_job_token_project_scope_link,
- source_project: project,
- target_project: other_project,
- added_by: user)
end
shared_examples 'can download code only' do
@@ -1474,19 +1471,16 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
- let(:build) { create(:ci_build, :running) }
- let(:other_project) { create(:project, :repository) }
-
- before do
- build.update!(project: project) # can't associate it on factory create
- create(:ci_job_token_project_scope_link,
- source_project: project,
- target_project: other_project,
- added_by: user)
+ let(:build) { create(:ci_build, :running, project: project, user: user) }
+ let(:other_project) do
+ create(:project, :repository).tap do |o|
+ make_project_fully_accessible(project, o)
+ end
end
# legacy behavior that is blocked/deprecated
context 'when build created by system is authenticated' do
+ let(:user) { nil }
let(:path) { "#{project.full_path}.git" }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
@@ -1505,7 +1499,6 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context 'and build created by' do
before do
- build.update!(user: user)
project.add_reporter(user)
end
@@ -1862,13 +1855,9 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
end
context 'from CI' do
- let(:build) { create(:ci_build, :running) }
+ let(:build) { create(:ci_build, :running, user: user, project: project) }
let(:env) { { user: 'gitlab-ci-token', password: build.token } }
- before do
- build.update!(user: user, project: project)
- end
-
it_behaves_like 'pulls are allowed'
end
end
diff --git a/spec/requests/groups/usage_quotas_controller_spec.rb b/spec/requests/groups/usage_quotas_controller_spec.rb
index 90fd08063f3..a329398aab3 100644
--- a/spec/requests/groups/usage_quotas_controller_spec.rb
+++ b/spec/requests/groups/usage_quotas_controller_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Groups::UsageQuotasController, :with_license, feature_category: :
request
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to match(/Placeholder for usage quotas Vue app/)
+ expect(response.body).to match(/js-usage-quotas-view/)
end
it 'renders 404 page if subgroup' do
diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb
index bf472469d85..7f0262eaf65 100644
--- a/spec/requests/jira_connect/public_keys_controller_spec.rb
+++ b/spec/requests/jira_connect/public_keys_controller_spec.rb
@@ -5,10 +5,11 @@ require 'spec_helper'
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 }
+ let(:public_key_storage_enabled_config) { true }
before do
- allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage).and_return(public_key_storage_enabled)
+ allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
+ .and_return(public_key_storage_enabled_config)
end
it 'renders 404' do
@@ -29,25 +30,25 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
expect(response.body).to eq(public_key.key)
end
- context 'when public key storage disabled' do
- let(:public_key_storage_enabled) { false }
+ context 'when public key storage config disabled' do
+ let(:public_key_storage_enabled_config) { false }
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
expect(response).to have_gitlab_http_status(:not_found)
end
- end
- context 'when jira_connect_oauth_self_managed disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
+ context 'when public key storage setting is enabled' do
+ before do
+ stub_application_setting(jira_connect_public_key_storage_enabled: true)
+ end
- it 'renders 404' do
- get jira_connect_public_key_path(id: uuid)
+ it 'renders 404' do
+ get jira_connect_public_key_path(id: uuid)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 49279024bd0..9035e723abe 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -192,7 +192,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_
end
it 'does not include any unknown properties' do
- expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time sub_legacy email email_verified groups_direct]
+ expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time sub_legacy name nickname preferred_username email email_verified website profile picture groups_direct]
end
it 'does include groups' do
@@ -276,7 +276,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
+ expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email]
end
context 'with a cross-origin request' do
@@ -286,7 +286,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[api read_user read_api read_repository write_repository sudo openid profile email]
+ expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email]
end
it_behaves_like 'cross-origin GET request'
diff --git a/spec/requests/profiles/keys_controller_spec.rb b/spec/requests/profiles/keys_controller_spec.rb
new file mode 100644
index 00000000000..48c382e6230
--- /dev/null
+++ b/spec/requests/profiles/keys_controller_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Profiles::KeysController, feature_category: :source_code_management do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'DELETE /-/profile/keys/:id/revoke' do
+ it 'returns 404 if a key not found' do
+ delete revoke_profile_key_path(non_existing_record_id)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'revokes ssh commit signatures' do
+ key = create(:key, user: user)
+ signature = create(:ssh_signature, key: key)
+
+ expect do
+ delete revoke_profile_key_path(signature.key)
+ end.to change { signature.reload.key }.from(signature.key).to(nil)
+ .and change { signature.verification_status }.from('verified').to('revoked_key')
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+end
diff --git a/spec/requests/profiles/saved_replies_controller_spec.rb b/spec/requests/profiles/saved_replies_controller_spec.rb
new file mode 100644
index 00000000000..27a961a201f
--- /dev/null
+++ b/spec/requests/profiles/saved_replies_controller_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ describe 'feature flag disabled' do
+ before do
+ stub_feature_flags(saved_replies: false)
+
+ get '/-/profile/saved_replies'
+ end
+
+ it { expect(response).to have_gitlab_http_status(:not_found) }
+ end
+
+ describe 'feature flag enabled' do
+ before do
+ get '/-/profile/saved_replies'
+ end
+
+ it { expect(response).to have_gitlab_http_status(:ok) }
+
+ it 'sets hide search settings ivar' do
+ expect(assigns(:hide_search_settings)).to eq(true)
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/airflow/dags_controller_spec.rb b/spec/requests/projects/airflow/dags_controller_spec.rb
new file mode 100644
index 00000000000..2dcedf5f128
--- /dev/null
+++ b/spec/requests/projects/airflow/dags_controller_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Airflow::DagsController, feature_category: :dataops do
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |p| p.add_developer(user) } }
+ let_it_be(:project) { create(:project, group: group).tap { |p| p.add_developer(user) } }
+
+ let(:current_user) { user }
+ let(:feature_flag) { true }
+
+ let_it_be(:dags) do
+ create_list(:airflow_dags, 5, project: project)
+ end
+
+ let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
+ let(:extra_params) { {} }
+
+ before do
+ sign_in(current_user) if current_user
+ stub_feature_flags(airflow_dags: false)
+ stub_feature_flags(airflow_dags: project) if feature_flag
+ list_dags
+ end
+
+ shared_examples 'returns a 404 if feature flag disabled' do
+ context 'when :airflow_dags disabled' do
+ let(:feature_flag) { false }
+
+ it 'is 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET index' do
+ it 'renders the template' do
+ expect(response).to render_template('projects/airflow/dags/index')
+ end
+
+ describe 'pagination' do
+ before do
+ stub_const("Projects::Airflow::DagsController::MAX_DAGS_PER_PAGE", 2)
+ dags
+
+ list_dags
+ end
+
+ context 'when out of bounds' do
+ let(:params) { extra_params.merge(page: 10000) }
+
+ it 'redirects to last page' do
+ last_page = (dags.size + 1) / 2
+ expect(response).to redirect_to(project_airflow_dags_path(project, page: last_page))
+ end
+ end
+
+ context 'when bad page' do
+ let(:params) { extra_params.merge(page: 's') }
+
+ it 'uses first page' do
+ expect(assigns(:pagination)).to include(
+ page: 1,
+ is_last_page: false,
+ per_page: 2,
+ total_items: dags.size)
+ end
+ end
+ end
+
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_dags }
+
+ create_list(:airflow_dags, 1, project: project)
+
+ expect { list_dags }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ it 'redirects to login' do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when user is not a member' do
+ let(:current_user) { non_member }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it_behaves_like 'returns a 404 if feature flag disabled'
+ end
+
+ private
+
+ def list_dags
+ get project_airflow_dags_path(project), params: params
+ end
+end
diff --git a/spec/requests/projects/blob_spec.rb b/spec/requests/projects/blob_spec.rb
new file mode 100644
index 00000000000..7d62619e76a
--- /dev/null
+++ b/spec/requests/projects/blob_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Blobs', feature_category: :source_code_management do
+ let_it_be(:project) { create(:project, :public, :repository, lfs: true) }
+
+ describe 'GET /:namespace_id/:project_id/-/blob/:id' do
+ subject(:request) do
+ get namespace_project_blob_path(namespace_id: project.namespace, project_id: project, id: id)
+ end
+
+ context 'with LFS file' do
+ let(:id) { 'master/files/lfs/lfs_object.iso' }
+ let(:object_store_host) { 'http://127.0.0.1:9000' }
+ let(:connect_src) do
+ csp = response.headers['Content-Security-Policy']
+ csp.split('; ').find { |src| src.starts_with?('connect-src') }
+ end
+
+ let(:gitlab_config) do
+ Gitlab.config.gitlab.deep_merge(
+ 'content_security_policy' => {
+ 'enabled' => content_security_policy_enabled
+ }
+ )
+ end
+
+ let(:lfs_config) do
+ Gitlab.config.lfs.deep_merge(
+ 'enabled' => lfs_enabled,
+ 'object_store' => {
+ 'remote_directory' => 'lfs-objects',
+ 'enabled' => true,
+ 'proxy_download' => proxy_download,
+ 'connection' => {
+ 'endpoint' => object_store_host,
+ 'path_style' => true
+ }
+ }
+ )
+ end
+
+ before do
+ stub_config_setting(gitlab_config)
+ stub_lfs_setting(lfs_config)
+ stub_lfs_object_storage(proxy_download: proxy_download)
+
+ request
+ end
+
+ describe 'directly downloading lfs file' do
+ let(:lfs_enabled) { true }
+ let(:proxy_download) { false }
+ let(:content_security_policy_enabled) { true }
+
+ it { expect(response).to have_gitlab_http_status(:success) }
+
+ it { expect(connect_src).to include(object_store_host) }
+
+ context 'when lfs is disabled' do
+ let(:lfs_enabled) { false }
+
+ it { expect(response).to have_gitlab_http_status(:success) }
+
+ it { expect(connect_src).not_to include(object_store_host) }
+ end
+
+ context 'when content_security_policy is disabled' do
+ let(:content_security_policy_enabled) { false }
+
+ it { expect(response).to have_gitlab_http_status(:success) }
+
+ it { expect(connect_src).not_to include(object_store_host) }
+ end
+
+ context 'when proxy download is enabled' do
+ let(:proxy_download) { true }
+
+ it { expect(response).to have_gitlab_http_status(:success) }
+
+ it { expect(connect_src).not_to include(object_store_host) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
index e91a51ce2ef..98e83610600 100644
--- a/spec/requests/projects/google_cloud/databases_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -94,23 +94,33 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca
post project_google_cloud_databases_path(project)
end
- it 'calls EnableCloudsqlService and redirects on error' do
- expect_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
- expect(service).to receive(:execute)
- .and_return({ status: :error, message: 'error' })
+ context 'when EnableCloudsqlService fails' do
+ before do
+ allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
end
- subject
+ it 'redirects and track event on error' do
+ subject
+
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DatabasesController',
+ action: 'error_enable_cloudsql_services',
+ label: nil,
+ project: project,
+ user: user
+ )
+ end
- expect(response).to redirect_to(project_google_cloud_databases_path(project))
+ it 'shows a flash alert' do
+ subject
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DatabasesController',
- action: 'error_enable_cloudsql_services',
- label: nil,
- project: project,
- user: user
- )
+ expect(flash[:alert]).to eq(s_('CloudSeed|Google Cloud Error - error'))
+ end
end
context 'when EnableCloudsqlService is successful' do
@@ -121,23 +131,33 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca
end
end
- it 'calls CreateCloudsqlInstanceService and redirects on error' do
- expect_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
- expect(service).to receive(:execute)
- .and_return({ status: :error, message: 'error' })
+ context 'when CreateCloudsqlInstanceService fails' do
+ before do
+ allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
end
- subject
+ it 'redirects and track event on error' do
+ subject
- expect(response).to redirect_to(project_google_cloud_databases_path(project))
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DatabasesController',
- action: 'error_create_cloudsql_instance',
- label: nil,
- project: project,
- user: user
- )
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DatabasesController',
+ action: 'error_create_cloudsql_instance',
+ label: nil,
+ project: project,
+ user: user
+ )
+ end
+
+ it 'shows a flash warning' do
+ subject
+
+ expect(flash[:warning]).to eq(s_('CloudSeed|Google Cloud Error - error'))
+ end
end
context 'when CreateCloudsqlInstanceService is successful' do
@@ -161,6 +181,18 @@ RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_ca
user: user
)
end
+
+ it 'shows a flash notice' do
+ subject
+
+ expect(flash[:notice])
+ .to eq(
+ s_(
+ 'CloudSeed|Cloud SQL instance creation request successful. ' \
+ 'Expected resolution time is ~5 minutes.'
+ )
+ )
+ end
end
end
end
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
index e8b6f806251..9b071efc1f1 100644
--- a/spec/requests/projects/ml/experiments_controller_spec.rb
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -38,31 +38,74 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
describe 'GET index' do
- before do
- list_experiments
- end
+ describe 'renderering' do
+ before do
+ list_experiments
+ end
- it 'renders the template' do
- expect(response).to render_template('projects/ml/experiments/index')
+ it 'renders the template' do
+ expect(response).to render_template('projects/ml/experiments/index')
+ end
+
+ it 'does not perform N+1 sql queries' do
+ 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)
+ end
end
- it 'does not perform N+1 sql queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_experiments }
+ describe 'pagination' do
+ let_it_be(:experiments) do
+ create_list(:ml_experiments, 3, project: project_with_feature)
+ end
- create_list(:ml_experiments, 2, project: project, user: user)
+ let(:params) { basic_params.merge(id: experiment.iid) }
- expect { list_experiments }.not_to exceed_all_query_limit(control_count)
+ before do
+ stub_const("Projects::Ml::ExperimentsController::MAX_EXPERIMENTS_PER_PAGE", 2)
+
+ list_experiments
+ end
+
+ it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
+ expect(assigns(:experiments).size).to eq(2)
+ end
+
+ it 'paginates', :aggregate_failures do
+ page = assigns(:experiments)
+
+ expect(page.first).to eq(experiments.last)
+ expect(page.last).to eq(experiments[1])
+
+ new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])
+
+ list_experiments(new_params)
+
+ new_page = assigns(:experiments)
+
+ expect(new_page.first).to eq(experiments.first)
+ end
end
context 'when :ml_experiment_tracking is disabled for the project' do
let(:project) { project_without_feature }
+ before do
+ list_experiments
+ end
+
it 'responds with a 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
- it_behaves_like '404 if feature flag disabled'
+ it_behaves_like '404 if feature flag disabled' do
+ before do
+ list_experiments
+ end
+ end
end
describe 'GET show' do
@@ -75,36 +118,85 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
describe 'pagination' do
- let_it_be(:candidates) { create_list(:ml_candidates, 5, experiment: experiment) }
+ let_it_be(:candidates) do
+ create_list(:ml_candidates, 5, experiment: experiment).tap do |c|
+ c.first.metrics.create!(name: 'metric1', value: 0.3)
+ c[1].metrics.create!(name: 'metric1', value: 0.2)
+ c.last.metrics.create!(name: 'metric1', value: 0.6)
+ end
+ end
+
+ let(:params) { basic_params.merge(id: experiment.iid) }
before do
stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2)
- candidates
show_experiment
end
- context 'when out of bounds' do
- let(:params) { basic_params.merge(id: experiment.iid, page: 10000) }
+ it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
+ expect(assigns(:candidates).size).to eq(2)
+ end
+
+ it 'paginates' do
+ received = assigns(:page_info)
- it 'redirects to last page' do
- last_page = (experiment.candidates.size + 1) / 2
+ expect(received).to include({
+ has_next_page: true,
+ has_previous_page: false,
+ start_cursor: nil
+ })
+ end
+
+ context 'when order by metric' do
+ let(:params) do
+ {
+ order_by: "metric1",
+ order_by_type: "metric",
+ sort: "desc"
+ }
+ end
+
+ it 'paginates', :aggregate_failures do
+ page = assigns(:candidates)
+
+ expect(page.first).to eq(candidates.last)
+ expect(page.last).to eq(candidates.first)
+
+ new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])
- expect(response).to redirect_to(project_ml_experiment_path(project, experiment.iid, page: last_page))
+ show_experiment(new_params)
+
+ new_page = assigns(:candidates)
+
+ expect(new_page.first).to eq(candidates[1])
end
end
+ end
- context 'when bad page' do
- let(:params) { basic_params.merge(id: experiment.iid, page: 's') }
+ describe 'search' do
+ let(:params) do
+ basic_params.merge(
+ id: experiment.iid,
+ name: 'some_name',
+ orderBy: 'name',
+ orderByType: 'metric',
+ sort: 'asc',
+ invalid: 'invalid'
+ )
+ end
- it 'uses first page' do
- expect(assigns(:pagination)).to include(
- page: 1,
- is_last_page: false,
- per_page: 2,
- total_items: experiment.candidates&.size
- )
+ it 'formats and filters the parameters' do
+ expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params|
+ expect(params.to_h).to include({
+ name: 'some_name',
+ order_by: 'name',
+ order_by_type: 'metric',
+ sort: 'asc'
+ })
end
+
+ show_experiment
end
end
@@ -125,11 +217,11 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
private
- def show_experiment
- get project_ml_experiment_path(project, experiment.iid), params: params
+ def show_experiment(new_params = nil)
+ get project_ml_experiment_path(project, experiment.iid), params: new_params || params
end
- def list_experiments
- get project_ml_experiments_path(project), params: params
+ def list_experiments(new_params = nil)
+ get project_ml_experiments_path(project), params: new_params || params
end
end
diff --git a/spec/requests/projects/network_controller_spec.rb b/spec/requests/projects/network_controller_spec.rb
index 954f9655558..dee95c6e70e 100644
--- a/spec/requests/projects/network_controller_spec.rb
+++ b/spec/requests/projects/network_controller_spec.rb
@@ -35,17 +35,6 @@ RSpec.describe Projects::NetworkController, feature_category: :source_code_manag
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
diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb
index 5699bf17b80..55540447da0 100644
--- a/spec/requests/projects/noteable_notes_spec.rb
+++ b/spec/requests/projects/noteable_notes_spec.rb
@@ -36,5 +36,41 @@ RSpec.describe 'Project noteable notes', feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:ok)
expect(response_etag).to eq(stored_etag)
end
+
+ it "instruments cache hits correctly" do
+ etag_store.touch(notes_path)
+
+ expect(Gitlab::Metrics::RailsSlis.request_apdex).to(
+ receive(:increment).with(
+ labels: {
+ request_urgency: :medium,
+ feature_category: "team_planning",
+ endpoint_id: "Projects::NotesController#index"
+ },
+ success: be_in([true, false])
+ )
+ )
+ allow(ActiveSupport::Notifications).to receive(:instrument).and_call_original
+
+ expect(ActiveSupport::Notifications).to(
+ receive(:instrument).with(
+ 'process_action.action_controller',
+ a_hash_including(
+ {
+ request_urgency: :medium,
+ target_duration_s: 0.5,
+ metadata: a_hash_including({
+ 'meta.feature_category' => 'team_planning',
+ 'meta.caller_id' => "Projects::NotesController#index"
+ })
+ }
+ )
+ )
+ )
+
+ get notes_path, headers: { "if-none-match": stored_etag }
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
end
end
diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb
index 7f185ade339..73e002b63b1 100644
--- a/spec/requests/projects/pipelines_controller_spec.rb
+++ b/spec/requests/projects/pipelines_controller_spec.rb
@@ -19,6 +19,32 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
login_as(user)
end
+ describe "GET index.json" do
+ it 'does not execute N+1 queries' do
+ get_pipelines_index
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get_pipelines_index
+ end.count
+
+ %w[pending running success failed canceled].each do |status|
+ create(:ci_pipeline, project: project, status: status)
+ end
+
+ # There appears to be one extra query for Pipelines#has_warnings? for some reason
+ expect { get_pipelines_index }.not_to exceed_query_limit(control_count + 1)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['pipelines'].count).to eq 6
+ end
+
+ def get_pipelines_index
+ get namespace_project_pipelines_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json)
+ end
+ end
+
describe "GET stages.json" do
it 'does not execute N+1 queries' do
request_build_stage
diff --git a/spec/requests/projects/releases_controller_spec.rb b/spec/requests/projects/releases_controller_spec.rb
index d331142583d..42fd55b5a43 100644
--- a/spec/requests/projects/releases_controller_spec.rb
+++ b/spec/requests/projects/releases_controller_spec.rb
@@ -8,17 +8,20 @@ RSpec.describe 'Projects::ReleasesController', feature_category: :release_orches
before do
project.add_developer(user)
- login_as(user)
end
# Added as a request spec because of https://gitlab.com/gitlab-org/gitlab/-/issues/232386
describe 'GET #downloads' do
- context 'filepath redirection' do
- let_it_be(:release) { create(:release, project: project, tag: 'v11.9.0-rc2' ) }
- let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') }
- let_it_be(:url) { "#{project_releases_path(project)}/#{release.tag}/downloads/bin/darwin-amd64" }
+ let_it_be(:release) { create(:release, project: project, tag: 'v11.9.0-rc2' ) }
+ let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') }
+ let_it_be(:url) { "#{project_releases_path(project)}/#{release.tag}/downloads/bin/darwin-amd64" }
- let(:subject) { get url }
+ let(:subject) { get url }
+
+ context 'filepath redirection' do
+ before do
+ login_as(user)
+ end
context 'valid filepath' do
let(:filepath) { '/bin/darwin-amd64' }
@@ -47,14 +50,29 @@ RSpec.describe 'Projects::ReleasesController', feature_category: :release_orches
end
end
- context 'invalid filepath' do
- let(:invalid_filepath) { 'bin/darwin-amd64' }
+ context 'sessionless download authentication' do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+ let(:filepath) { '/bin/darwin-amd64' }
+
+ subject { get url, params: { private_token: personal_access_token.token } }
- let(:subject) { create(:release_link, name: 'linux-amd64 binaries', filepath: invalid_filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') }
+ it 'will allow sessionless users to download the file' do
+ subject
- it 'cannot create an invalid filepath' do
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ expect(controller.current_user).to eq(user)
+ expect(response).to have_gitlab_http_status(:redirect)
+ expect(response).to redirect_to(link.url)
end
end
end
+
+ context 'invalid filepath' do
+ let(:invalid_filepath) { 'bin/darwin-amd64' }
+
+ let(:subject) { create(:release_link, name: 'linux-amd64 binaries', filepath: invalid_filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') }
+
+ it 'cannot create an invalid filepath' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
end
diff --git a/spec/requests/pwa_controller_spec.rb b/spec/requests/pwa_controller_spec.rb
index a80d083c11f..08eeefd1dc4 100644
--- a/spec/requests/pwa_controller_spec.rb
+++ b/spec/requests/pwa_controller_spec.rb
@@ -4,27 +4,74 @@ require 'spec_helper'
RSpec.describe PwaController, feature_category: :navigation do
describe 'GET #manifest' do
- it 'responds with json' do
- get manifest_path(format: :json)
+ shared_examples 'text values' do |params, result|
+ let_it_be(:appearance) { create(:appearance, **params) }
- expect(response.body).to include('The complete DevOps platform.')
- expect(Gitlab::Json.parse(response.body)).to include({ 'short_name' => 'GitLab' })
- expect(response).to have_gitlab_http_status(:success)
+ it 'uses custom values', :aggregate_failures do
+ get manifest_path(format: :json)
+
+ expect(Gitlab::Json.parse(response.body)).to include(result)
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'with default appearance' do
+ it_behaves_like 'text values', {}, {
+ 'name' => 'GitLab',
+ 'short_name' => 'GitLab',
+ 'description' => 'The complete DevOps platform. ' \
+ 'One application with endless possibilities. ' \
+ 'Organizations rely on GitLab’s source code management, ' \
+ 'CI/CD, security, and more to deliver software rapidly.'
+ }
end
context 'with customized appearance' do
- let_it_be(:appearance) do
- create(:appearance, title: 'Long name', pwa_short_name: 'Short name', description: 'This is a test')
+ context 'with custom text values' do
+ it_behaves_like 'text values', { pwa_name: 'PWA name' }, { 'name' => 'PWA name' }
+ it_behaves_like 'text values', { pwa_short_name: 'Short name' }, { 'short_name' => 'Short name' }
+ it_behaves_like 'text values', { pwa_description: 'This is a test' }, { 'description' => 'This is a test' }
end
- it 'uses custom values', :aggregate_failures do
- get manifest_path(format: :json)
+ shared_examples 'icon paths' do
+ it 'returns expected icon paths', :aggregate_failures do
+ get manifest_path(format: :json)
+
+ expect(Gitlab::Json.parse(response.body)["icons"]).to match_array(result)
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'with custom icon' do
+ let_it_be(:appearance) { create(:appearance, :with_pwa_icon) }
+ let_it_be(:result) do
+ [{ "src" => "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=192", "sizes" => "192x192",
+ "type" => "image/png" },
+ { "src" => "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=512", "sizes" => "512x512",
+ "type" => "image/png" }]
+ end
+
+ it_behaves_like 'icon paths'
+ end
- expect(Gitlab::Json.parse(response.body)).to include({
- 'description' => 'This is a test',
- 'name' => 'Long name',
- 'short_name' => 'Short name'
- })
+ context 'with no custom icon' do
+ let_it_be(:appearance) { create(:appearance) }
+ let_it_be(:result) do
+ [{ "src" => "/-/pwa-icons/logo-192.png", "sizes" => "192x192", "type" => "image/png" },
+ { "src" => "/-/pwa-icons/logo-512.png", "sizes" => "512x512", "type" => "image/png" },
+ { "src" => "/-/pwa-icons/maskable-logo.png", "sizes" => "512x512", "type" => "image/png",
+ "purpose" => "maskable" }]
+ end
+
+ it_behaves_like 'icon paths'
+ end
+ end
+
+ describe 'GET #offline' do
+ it 'responds with static HTML page' do
+ get offline_path
+
+ expect(response.body).to include('You are currently offline')
expect(response).to have_gitlab_http_status(:success)
end
end
@@ -46,13 +93,4 @@ RSpec.describe PwaController, feature_category: :navigation do
end
end
end
-
- describe 'GET #offline' do
- it 'responds with static HTML page' do
- get offline_path
-
- expect(response.body).to include('You are currently offline')
- expect(response).to have_gitlab_http_status(:success)
- end
- end
end
diff --git a/spec/requests/user_activity_spec.rb b/spec/requests/user_activity_spec.rb
index f9682d81640..16188ab6a41 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', feature_category: :users do
+RSpec.describe 'Update of user activity', feature_category: :user_profile do
paths_to_visit = [
'/group',
'/group/project',
diff --git a/spec/requests/user_avatar_spec.rb b/spec/requests/user_avatar_spec.rb
index 4e3c2744d56..0a9f3784833 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', feature_category: :users do
+RSpec.describe 'Loading a user avatar', feature_category: :user_profile do
let(:user) { create(:user, :with_avatar) }
context 'when logged in' do
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index cac754a9cb1..8a6a7e717ff 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -223,6 +223,7 @@ feature_category: :user_management do
context 'when the feature flag is toggled on' do
before do
stub_feature_flags(require_email_verification: user)
+ stub_feature_flags(skip_require_email_verification: false)
end
it_behaves_like 'verifying with email'
@@ -242,6 +243,14 @@ feature_category: :user_management do
it_behaves_like 'verifying with email'
end
+
+ context 'when the skip_require_email_verification feature flag is turned on' do
+ before do
+ stub_feature_flags(skip_require_email_verification: user)
+ end
+
+ it_behaves_like 'not verifying with email'
+ end
end
end
end
diff --git a/spec/routing/directs/milestone_spec.rb b/spec/routing/directs/milestone_spec.rb
new file mode 100644
index 00000000000..26a5bd4902b
--- /dev/null
+++ b/spec/routing/directs/milestone_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Custom URLs', 'milestone', feature_category: :team_planning do
+ describe 'milestone' do
+ context 'with project' do
+ let(:project) { milestone.project }
+ let(:milestone) { build_stubbed(:milestone, :on_project) }
+
+ it 'creates directs' do
+ expect(milestone_path(milestone)).to eq(project_milestone_path(project, milestone))
+ expect(milestone_url(milestone)).to eq(project_milestone_url(project, milestone))
+ end
+ end
+
+ context 'with group' do
+ let(:group) { milestone.group }
+ let(:milestone) { build_stubbed(:milestone, :on_group) }
+
+ it 'creates directs' do
+ expect(milestone_path(milestone)).to eq(group_milestone_path(group, milestone))
+ expect(milestone_url(milestone)).to eq(group_milestone_url(group, milestone))
+ end
+ end
+ end
+end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index b63ae1e7e4e..ac3f2a4b7ca 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -71,6 +71,10 @@ RSpec.describe Import::GithubController, 'routing' do
it 'to #personal_access_token' do
expect(post('/import/github/personal_access_token')).to route_to('import/github#personal_access_token')
end
+
+ it 'to #cancel_all' do
+ expect(post('/import/github/cancel_all')).to route_to('import/github#cancel_all')
+ end
end
# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 1b6a5182531..664fc7dde7a 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -843,10 +843,6 @@ RSpec.describe 'project routing' do
end
describe Projects::ServicePingController, 'routing' do
- it 'routes to service_ping#web_ide_clientside_preview' do
- expect(post('/gitlab/gitlabhq/service_ping/web_ide_clientside_preview')).to route_to('projects/service_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
-
it 'routes to service_ping#web_ide_pipelines_count' do
expect(post('/gitlab/gitlabhq/service_ping/web_ide_pipelines_count')).to route_to('projects/service_ping#web_ide_pipelines_count', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
diff --git a/spec/rubocop/cop/gitlab/doc_url_spec.rb b/spec/rubocop/cop/gitlab/doc_url_spec.rb
new file mode 100644
index 00000000000..4a7ef14ccbc
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/doc_url_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/gitlab/doc_url'
+
+RSpec.describe RuboCop::Cop::Gitlab::DocUrl, feature_category: :not_owned do
+ context 'when string literal is added with docs url prefix' do
+ context 'when inlined' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ 'See [the docs](https://docs.gitlab.com/ee/user/permissions#roles).'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ RUBY
+ end
+ end
+
+ context 'when multilined' do
+ it 'registers an offense' do
+ expect_offense(<<~'RUBY')
+ 'See the docs: ' \
+ 'https://docs.gitlab.com/ee/user/permissions#roles'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ RUBY
+ end
+ end
+
+ context 'with heredoc' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ <<-HEREDOC
+ See the docs:
+ https://docs.gitlab.com/ee/user/permissions#roles
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `#help_page_url` instead of directly including link. See https://docs.gitlab.com/ee/development/documentation/#linking-to-help-in-ruby.
+ HEREDOC
+ RUBY
+ end
+ end
+ end
+
+ context 'when string literal is added without docs url prefix' do
+ context 'when inlined' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ '[The DevSecOps Platform](https://about.gitlab.com/)'
+ RUBY
+ end
+ end
+
+ context 'when multilined' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ 'The DevSecOps Platform: ' \
+ 'https://about.gitlab.com/'
+ RUBY
+ end
+ end
+
+ context 'with heredoc' do
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ <<-HEREDOC
+ The DevSecOps Platform:
+ https://about.gitlab.com/
+ HEREDOC
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb b/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb
new file mode 100644
index 00000000000..968de7d2160
--- /dev/null
+++ b/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction'
+
+RSpec.describe RuboCop::Cop::Migration::PreventSingleStatementWithDisableDdlTransaction, feature_category: :database do
+ context 'when in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when `disable_ddl_transaction!` is only for the :validate_foreign_key statement' do
+ code = <<~RUBY
+ class SomeMigration < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+ def up
+ validate_foreign_key :emails, :user_id
+ end
+ def down
+ # no-op
+ end
+ end
+ RUBY
+
+ expect_offense(<<~RUBY, node: code, msg: described_class::MSG)
+ class SomeMigration < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+ ^^^^^^^^^^^^^^^^^^^^^^^^ %{msg}
+ def up
+ validate_foreign_key :emails, :user_id
+ end
+ def down
+ # no-op
+ end
+ end
+ RUBY
+ end
+
+ it 'registers no offense when `disable_ddl_transaction!` is used with more than one statement' do
+ expect_no_offenses(<<~RUBY)
+ class SomeMigration < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+ def up
+ add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false
+ validate_foreign_key :emails, :user_id
+ end
+ def down
+ remove_foreign_key_if_exists :emails, column: :user_id
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when outside of migration' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class SomeMigration
+ disable_ddl_transaction!
+ def up
+ validate_foreign_key :deployments, :environment_id
+ end
+ end
+ RUBY
+ end
+ 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 506e3146afa..332b02078f4 100644
--- a/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
+++ b/spec/rubocop/cop/migration/versioned_migration_class_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/migration/versioned_migration_class'
-RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass do
+RSpec.describe RuboCop::Cop::Migration::VersionedMigrationClass, feature_category: :database do
let(:migration) do
<<~SOURCE
class TestMigration < Gitlab::Database::Migration[2.1]
@@ -49,7 +49,15 @@ 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[2.1] instead. See https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning.
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. 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 inheriting from old version of Gitlab::Database::Migration' do
+ expect_offense(<<~RUBY)
+ class MyMigration < Gitlab::Database::Migration[2.0]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. 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
diff --git a/spec/rubocop/cop/rspec/env_mocking_spec.rb b/spec/rubocop/cop/rspec/env_mocking_spec.rb
new file mode 100644
index 00000000000..189fccf483a
--- /dev/null
+++ b/spec/rubocop/cop/rspec/env_mocking_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/env_mocking'
+
+RSpec.describe RuboCop::Cop::RSpec::EnvMocking, feature_category: :tooling do
+ offense_call_brackets_string_quotes = %(allow(ENV).to receive(:[]).with('FOO').and_return('bar'))
+ offense_call_brackets_variables = %(allow(ENV).to receive(:[]).with(key).and_return(value))
+
+ offense_call_fetch_string_quotes = %(allow(ENV).to receive(:fetch).with('FOO').and_return('bar'))
+ offense_call_fetch_variables = %(allow(ENV).to receive(:fetch).with(key).and_return(value))
+
+ offense_call_root_env_variables = %(allow(::ENV).to receive(:[]).with(key).and_return(value))
+ offense_call_key_value_method_calls =
+ %(allow(ENV).to receive(:[]).with(fetch_key(object)).and_return(fetch_value(object)))
+
+ acceptable_mocking_other_methods = %(allow(ENV).to receive(:foo).with("key").and_return("value"))
+
+ let(:source_file) { 'spec/foo_spec.rb' }
+
+ shared_examples 'cop offense mocking the ENV constant correctable with stub_env' do |content, autocorrected_content|
+ it "registers an offense for `#{content}` and corrects", :aggregate_failures do
+ expect_offense(<<~CODE, content: content)
+ %{content}
+ ^{content} Don't mock the ENV, use `stub_env` instead.
+ CODE
+
+ expect_correction(<<~CODE)
+ #{autocorrected_content}
+ CODE
+ end
+ end
+
+ context 'with mocking bracket calls ' do
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_brackets_string_quotes, %(stub_env('FOO', 'bar'))
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_brackets_variables, %(stub_env(key, value))
+ end
+
+ context 'with mocking fetch calls' do
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_fetch_string_quotes, %(stub_env('FOO', 'bar'))
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_fetch_variables, %(stub_env(key, value))
+ end
+
+ context 'with other special cases and variations' do
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_root_env_variables, %(stub_env(key, value))
+ it_behaves_like 'cop offense mocking the ENV constant correctable with stub_env',
+ offense_call_key_value_method_calls, %(stub_env(fetch_key(object), fetch_value(object)))
+ end
+
+ context 'with acceptable cases' do
+ it 'does not register an offense for mocking other methods' do
+ expect_no_offenses(acceptable_mocking_other_methods)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/invalid_feature_category_spec.rb b/spec/rubocop/cop/rspec/invalid_feature_category_spec.rb
new file mode 100644
index 00000000000..0d2fd029a13
--- /dev/null
+++ b/spec/rubocop/cop/rspec/invalid_feature_category_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../../rubocop/cop/rspec/invalid_feature_category'
+
+RSpec.describe RuboCop::Cop::RSpec::InvalidFeatureCategory, feature_category: :tooling do
+ shared_examples 'feature category validation' do |valid_category|
+ it 'flags invalid feature category in top level example group' do
+ expect_offense(<<~RUBY, invalid: invalid_category)
+ RSpec.describe 'foo', feature_category: :%{invalid}, foo: :bar do
+ ^^{invalid} Please use a valid feature category. See https://docs.gitlab.com/ee/development/feature_categorization/#rspec-examples.
+ end
+ RUBY
+ end
+
+ it 'flags invalid feature category in nested context' do
+ expect_offense(<<~RUBY, valid: valid_category, invalid: invalid_category)
+ RSpec.describe 'foo', feature_category: :%{valid} do
+ context 'bar', foo: :bar, feature_category: :%{invalid} do
+ ^^{invalid} Please use a valid feature category. See https://docs.gitlab.com/ee/development/feature_categorization/#rspec-examples.
+ end
+ end
+ RUBY
+ end
+
+ it 'flags invalid feature category in examples' do
+ expect_offense(<<~RUBY, valid: valid_category, invalid: invalid_category)
+ RSpec.describe 'foo', feature_category: :%{valid} do
+ it 'bar', feature_category: :%{invalid} do
+ ^^{invalid} Please use a valid feature category. See https://docs.gitlab.com/ee/development/feature_categorization/#rspec-examples.
+ end
+ end
+ RUBY
+ end
+
+ it 'does not flag if feature category is valid' do
+ expect_no_offenses(<<~RUBY)
+ RSpec.describe 'foo', feature_category: :#{valid_category} do
+ context 'bar', feature_category: :#{valid_category} do
+ it 'baz', feature_category: :#{valid_category} do
+ end
+ end
+ end
+ RUBY
+ end
+
+ it 'suggests an alternative' do
+ mistyped = make_typo(valid_category)
+
+ expect_offense(<<~RUBY, invalid: mistyped, valid: valid_category)
+ RSpec.describe 'foo', feature_category: :%{invalid} do
+ ^^{invalid} Please use a valid feature category. Did you mean `:%{valid}`? See [...]
+ end
+ RUBY
+ end
+
+ def make_typo(string)
+ "#{string}#{string[-1]}"
+ end
+ end
+
+ let(:invalid_category) { :invalid_category }
+
+ context 'with categories defined in config/feature_categories.yml' do
+ where(:valid_category) do
+ YAML.load_file(rails_root_join('config/feature_categories.yml'))
+ end
+
+ with_them do
+ it_behaves_like 'feature category validation', params[:valid_category]
+ end
+ end
+
+ context 'with custom categories' do
+ it_behaves_like 'feature category validation', 'tooling'
+ it_behaves_like 'feature category validation', 'shared'
+ end
+
+ it 'flags invalid feature category for non-symbols' do
+ expect_offense(<<~RUBY, invalid: invalid_category)
+ RSpec.describe 'foo', feature_category: "%{invalid}" do
+ ^^^{invalid} Please use a symbol as value.
+ end
+
+ RSpec.describe 'foo', feature_category: 42 do
+ ^^ Please use a symbol as value.
+ end
+ RUBY
+ end
+
+ it 'does not flag use of invalid categories in non-example code' do
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/381882#note_1265865125
+ expect_no_offenses(<<~RUBY)
+ RSpec.describe 'A spec' do
+ let(:api_handler) do
+ Class.new(described_class) do
+ namespace '/test' do
+ get 'hello', feature_category: :foo, urgency: :#{invalid_category} do
+ end
+ end
+ end
+ end
+
+ it 'tests something' do
+ Gitlab::ApplicationContext.with_context(feature_category: :#{invalid_category}) do
+ payload = generator.generate(exception, extra)
+ end
+ end
+ end
+ RUBY
+ end
+
+ describe '#external_dependency_checksum' do
+ it 'returns a SHA256 digest used by RuboCop to invalid cache' do
+ expect(cop.external_dependency_checksum).to match(/^\h{64}$/)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/missing_feature_category_spec.rb b/spec/rubocop/cop/rspec/missing_feature_category_spec.rb
new file mode 100644
index 00000000000..41b1d2b8580
--- /dev/null
+++ b/spec/rubocop/cop/rspec/missing_feature_category_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/rspec/missing_feature_category'
+
+RSpec.describe RuboCop::Cop::RSpec::MissingFeatureCategory, feature_category: :tooling do
+ it 'flags missing feature category in top level example group' do
+ expect_offense(<<~RUBY)
+ RSpec.describe 'foo' do
+ ^^^^^^^^^^^^^^^^^^^^ Please add missing feature category. See https://docs.gitlab.com/ee/development/feature_categorization/#rspec-examples.
+ end
+
+ RSpec.describe 'foo', some: :tag do
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Please add missing feature category. See https://docs.gitlab.com/ee/development/feature_categorization/#rspec-examples.
+ end
+ RUBY
+ end
+
+ it 'does not flag if feature category is defined' do
+ expect_no_offenses(<<~RUBY)
+ RSpec.describe 'foo', feature_category: :foo do
+ end
+
+ RSpec.describe 'foo', some: :tag, feature_category: :foo do
+ end
+
+ RSpec.describe 'foo', feature_category: :foo, some: :tag do
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
index 1395615479f..43ac9457ed6 100644
--- a/spec/rubocop/cop/scalability/file_uploads_spec.rb
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -3,7 +3,7 @@
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/scalability/file_uploads'
-RSpec.describe RuboCop::Cop::Scalability::FileUploads do
+RSpec.describe RuboCop::Cop::Scalability::FileUploads, feature_category: :scalability do
let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
context 'with required params' do
diff --git a/spec/rubocop/migration_helpers_spec.rb b/spec/rubocop/migration_helpers_spec.rb
index 6e6c3a7a0b9..56b7b6aa76b 100644
--- a/spec/rubocop/migration_helpers_spec.rb
+++ b/spec/rubocop/migration_helpers_spec.rb
@@ -2,6 +2,7 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
+require 'rubocop/ast'
require_relative '../../rubocop/migration_helpers'
@@ -69,4 +70,27 @@ RSpec.describe RuboCop::MigrationHelpers do
it { expect(fake_cop.time_enforced?(node)).to eq(expected) }
end
end
+
+ describe '#array_column?' do
+ let(:name) { nil }
+ let(:node) { double(:node, each_descendant: [pair_node]) }
+ let(:pair_node) { double(child_nodes: child_nodes) }
+
+ context 'when it matches array: true' do
+ let(:child_nodes) do
+ [
+ RuboCop::AST::SymbolNode.new(:sym, [:array]),
+ RuboCop::AST::Node.new(:true) # rubocop:disable Lint/BooleanSymbol
+ ]
+ end
+
+ it { expect(fake_cop.array_column?(node)).to eq(true) }
+ end
+
+ context 'when it matches a variable => 100' do
+ let(:child_nodes) { [RuboCop::AST::Node.new(:lvar, [:variable]), RuboCop::AST::IntNode.new(:int, [100])] }
+
+ it { expect(fake_cop.array_column?(node)).to eq(false) }
+ end
+ end
end
diff --git a/spec/scripts/failed_tests_spec.rb b/spec/scripts/failed_tests_spec.rb
index b99fd991c55..ce0ec66cdb6 100644
--- a/spec/scripts/failed_tests_spec.rb
+++ b/spec/scripts/failed_tests_spec.rb
@@ -5,121 +5,113 @@ require_relative '../../scripts/failed_tests'
RSpec.describe FailedTests do
let(:report_file) { 'spec/fixtures/scripts/test_report.json' }
- let(:output_directory) { 'tmp/previous_test_results' }
- let(:rspec_pg_regex) { /rspec .+ pg12( .+)?/ }
- let(:rspec_ee_pg_regex) { /rspec-ee .+ pg12( .+)?/ }
-
- subject { described_class.new(previous_tests_report_path: report_file, output_directory: output_directory, rspec_pg_regex: rspec_pg_regex, rspec_ee_pg_regex: rspec_ee_pg_regex) }
-
- describe '#output_failed_test_files' do
- it 'writes the file for the suite' do
- expect(File).to receive(:open).with(File.join(output_directory, "rspec_failed_files.txt"), 'w').once
-
- subject.output_failed_test_files
- end
- end
-
- describe '#failed_files_for_suite_collection' do
- let(:failure_path) { 'path/to/fail_file_spec.rb' }
- let(:other_failure_path) { 'path/to/fail_file_spec_2.rb' }
- let(:file_contents_as_json) do
- {
- 'suites' => [
- {
- 'failed_count' => 1,
- 'name' => 'rspec unit pg12 10/12',
- 'test_cases' => [
- {
- 'status' => 'failed',
- 'file' => failure_path
- }
- ]
- },
- {
- 'failed_count' => 1,
- 'name' => 'rspec-ee unit pg12',
- 'test_cases' => [
- {
- 'status' => 'failed',
- 'file' => failure_path
- }
- ]
- },
- {
- 'failed_count' => 1,
- 'name' => 'rspec unit pg13 10/12',
- 'test_cases' => [
- {
- 'status' => 'failed',
- 'file' => other_failure_path
- }
- ]
- }
- ]
- }
- end
-
- before do
- allow(subject).to receive(:file_contents_as_json).and_return(file_contents_as_json)
- end
-
- it 'returns a list of failed file paths for suite collection' do
- result = subject.failed_files_for_suite_collection
-
- expect(result[:rspec].to_a).to match_array(failure_path)
- expect(result[:rspec_ee].to_a).to match_array(failure_path)
- end
+ let(:options) { described_class::DEFAULT_OPTIONS.merge(previous_tests_report_path: report_file) }
+ let(:failure_path) { 'path/to/fail_file_spec.rb' }
+ let(:other_failure_path) { 'path/to/fail_file_spec_2.rb' }
+ let(:file_contents_as_json) do
+ {
+ 'suites' => [
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec unit pg12 10/12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => failure_path
+ }
+ ]
+ },
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec-ee unit pg12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => failure_path
+ }
+ ]
+ },
+ {
+ 'failed_count' => 1,
+ 'name' => 'rspec unit pg13 10/12',
+ 'test_cases' => [
+ {
+ 'status' => 'failed',
+ 'file' => other_failure_path
+ }
+ ]
+ }
+ ]
+ }
end
- describe 'empty report' do
- let(:file_content) do
- '{}'
- end
-
- before do
- allow(subject).to receive(:file_contents).and_return(file_content)
- end
-
- it 'does not fail for output files' do
- subject.output_failed_test_files
- end
-
- it 'returns empty results for suite failures' do
- result = subject.failed_files_for_suite_collection
-
- expect(result.values.flatten).to be_empty
- end
- end
-
- describe 'invalid report' do
- let(:file_content) do
- ''
- end
-
- before do
- allow(subject).to receive(:file_contents).and_return(file_content)
- end
-
- it 'does not fail for output files' do
- subject.output_failed_test_files
- end
-
- it 'returns empty results for suite failures' do
- result = subject.failed_files_for_suite_collection
-
- expect(result.values.flatten).to be_empty
+ subject { described_class.new(options) }
+
+ describe '#output_failed_tests' do
+ context 'with a valid report file' do
+ before do
+ allow(subject).to receive(:file_contents_as_json).and_return(file_contents_as_json)
+ end
+
+ it 'writes the file for the suite' do
+ expect(File).to receive(:open)
+ .with(File.join(described_class::DEFAULT_OPTIONS[:output_directory], "rspec_failed_tests.txt"), 'w').once
+ expect(File).to receive(:open)
+ .with(File.join(described_class::DEFAULT_OPTIONS[:output_directory], "rspec_ee_failed_tests.txt"), 'w').once
+
+ subject.output_failed_tests
+ end
+
+ context 'when given a valid format' do
+ subject { described_class.new(options.merge(format: :json)) }
+
+ it 'writes the file for the suite' do
+ expect(File).to receive(:open)
+ .with(File.join(described_class::DEFAULT_OPTIONS[:output_directory], "rspec_failed_tests.json"), 'w').once
+ expect(File).to receive(:open)
+ .with(File.join(described_class::DEFAULT_OPTIONS[:output_directory], "rspec_ee_failed_tests.json"), 'w')
+ .once
+
+ subject.output_failed_tests
+ end
+ end
+
+ context 'when given an invalid format' do
+ subject { described_class.new(options.merge(format: :foo)) }
+
+ it 'raises an exception' do
+ expect { subject.output_failed_tests }
+ .to raise_error '[FailedTests] Unsupported format `foo` (allowed formats: `oneline` and `json`)!'
+ end
+ end
+
+ describe 'empty report' do
+ let(:file_contents_as_json) do
+ {}
+ end
+
+ it 'does not fail for output files' do
+ subject.output_failed_tests
+ end
+
+ it 'returns empty results for suite failures' do
+ result = subject.failed_cases_for_suite_collection
+
+ expect(result.values.flatten).to be_empty
+ end
+ end
end
end
describe 'missing report file' do
- let(:report_file) { 'unknownfile.json' }
+ subject { described_class.new(options.merge(previous_tests_report_path: 'unknownfile.json')) }
it 'does not fail for output files' do
- subject.output_failed_test_files
+ subject.output_failed_tests
end
it 'returns empty results for suite failures' do
- result = subject.failed_files_for_suite_collection
+ result = subject.failed_cases_for_suite_collection
expect(result.values.flatten).to be_empty
end
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index 58e016b6d68..bfc25877f98 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -28,7 +28,7 @@ require_relative '../../../../scripts/lib/glfm/update_example_snapshots'
# Also, the textual content of the individual fixture file entries is also crafted to help
# indicate which scenarios which they are covering.
# rubocop:disable RSpec/MultipleMemoizedHelpers
-RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
+RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team_planning do
subject { described_class.new }
# GLFM input files
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index ed5650e7310..92434b37515 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -26,7 +26,7 @@ require_relative '../../../support/helpers/next_instance_of'
# should run in sub-second time when the Spring pre-loader is used. This allows
# logic which is not directly related to the slow sub-processes to be TDD'd with a
# very rapid feedback cycle.
-RSpec.describe Glfm::UpdateSpecification, '#process' do
+RSpec.describe Glfm::UpdateSpecification, '#process', feature_category: :team_planning do
include NextInstanceOf
subject { described_class.new }
diff --git a/spec/scripts/pipeline_test_report_builder_spec.rb b/spec/scripts/pipeline_test_report_builder_spec.rb
index b51b4dc4887..e7529eb0d41 100644
--- a/spec/scripts/pipeline_test_report_builder_spec.rb
+++ b/spec/scripts/pipeline_test_report_builder_spec.rb
@@ -3,12 +3,11 @@
require 'fast_spec_helper'
require_relative '../../scripts/pipeline_test_report_builder'
-RSpec.describe PipelineTestReportBuilder do
+RSpec.describe PipelineTestReportBuilder, feature_category: :tooling do
let(:report_file) { 'spec/fixtures/scripts/test_report.json' }
let(:output_file_path) { 'tmp/previous_test_results/output_file.json' }
-
- subject do
- described_class.new(
+ let(:options) do
+ described_class::DEFAULT_OPTIONS.merge(
target_project: 'gitlab-org/gitlab',
mr_id: '999',
instance_base_url: 'https://gitlab.com',
@@ -16,25 +15,27 @@ RSpec.describe PipelineTestReportBuilder do
)
end
- let(:failed_pipeline_url) { 'pipeline2_url' }
+ let(:previous_pipeline_url) { '/pipelines/previous' }
- let(:failed_pipeline) do
+ let(:previous_pipeline) do
{
'status' => 'failed',
- 'created_at' => (DateTime.now - 5).to_s,
- 'web_url' => failed_pipeline_url
+ 'id' => 1,
+ 'web_url' => previous_pipeline_url
}
end
- let(:current_pipeline) do
+ let(:latest_pipeline_url) { '/pipelines/latest' }
+
+ let(:latest_pipeline) do
{
'status' => 'running',
- 'created_at' => DateTime.now.to_s,
- 'web_url' => 'pipeline1_url'
+ 'id' => 3,
+ 'web_url' => latest_pipeline_url
}
end
- let(:mr_pipelines) { [current_pipeline, failed_pipeline] }
+ let(:mr_pipelines) { [latest_pipeline, previous_pipeline] }
let(:failed_build_id) { 9999 }
@@ -68,6 +69,8 @@ RSpec.describe PipelineTestReportBuilder do
}
end
+ subject { described_class.new(options) }
+
before do
allow(subject).to receive(:pipelines_for_mr).and_return(mr_pipelines)
allow(subject).to receive(:failed_builds_for_pipeline).and_return(failed_builds_for_pipeline)
@@ -78,7 +81,7 @@ RSpec.describe PipelineTestReportBuilder do
let(:fork_pipeline) do
{
'status' => 'failed',
- 'created_at' => (DateTime.now - 5).to_s,
+ 'id' => 2,
'web_url' => fork_pipeline_url
}
end
@@ -88,7 +91,7 @@ RSpec.describe PipelineTestReportBuilder do
end
context 'pipeline in a fork project' do
- let(:mr_pipelines) { [current_pipeline, fork_pipeline] }
+ let(:mr_pipelines) { [latest_pipeline, fork_pipeline] }
it 'returns fork pipeline' do
expect(subject.previous_pipeline).to eq(fork_pipeline)
@@ -97,125 +100,105 @@ RSpec.describe PipelineTestReportBuilder do
context 'pipeline in target project' do
it 'returns failed pipeline' do
- expect(subject.previous_pipeline).to eq(failed_pipeline)
+ expect(subject.previous_pipeline).to eq(previous_pipeline)
end
end
end
- describe '#test_report_for_latest_pipeline' do
- let(:failed_build_uri) { "#{failed_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}" }
-
- before do
- allow(subject).to receive(:fetch).with(failed_build_uri).and_return(failed_builds_for_pipeline)
- end
-
- it 'fetches builds from pipeline related to MR' do
- expected = { "suites" => [failed_builds_for_pipeline] }.to_json
- expect(subject.test_report_for_latest_pipeline).to eq(expected)
- end
-
- context 'canonical pipeline' do
- context 'no previous pipeline' do
- let(:mr_pipelines) { [] }
+ describe '#test_report_for_pipeline' do
+ context 'for previous pipeline' do
+ let(:failed_build_uri) { "#{previous_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}" }
- it 'returns empty hash' do
- expect(subject.test_report_for_latest_pipeline).to eq("{}")
- end
+ before do
+ allow(subject).to receive(:fetch).with(failed_build_uri).and_return(test_report_for_build)
end
- context 'first pipeline scenario' do
- let(:mr_pipelines) do
- [
- {
- 'status' => 'running',
- 'created_at' => DateTime.now.to_s
- }
- ]
- end
-
- it 'returns empty hash' do
- expect(subject.test_report_for_latest_pipeline).to eq("{}")
- end
+ it 'fetches builds from pipeline related to MR' do
+ expected = { "suites" => [test_report_for_build.merge('job_url' => "/jobs/#{failed_build_id}")] }.to_json
+ expect(subject.test_report_for_pipeline).to eq(expected)
end
- context 'no previous failed pipeline' do
- let(:mr_pipelines) do
- [
- {
- 'status' => 'running',
- 'created_at' => DateTime.now.to_s
- },
- {
- 'status' => 'success',
- 'created_at' => (DateTime.now - 5).to_s
- }
- ]
- end
+ context 'canonical pipeline' do
+ context 'no previous pipeline' do
+ let(:mr_pipelines) { [] }
- it 'returns empty hash' do
- expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ it 'returns empty hash' do
+ expect(subject.test_report_for_pipeline).to eq("{}")
+ end
end
- end
- context 'no failed test builds' do
- let(:failed_builds_for_pipeline) do
- [
- {
- 'id' => 9999,
- 'stage' => 'prepare'
- }
- ]
- end
+ context 'no failed test builds' do
+ let(:failed_builds_for_pipeline) do
+ [
+ {
+ 'id' => 9999,
+ 'stage' => 'prepare'
+ }
+ ]
+ end
- it 'returns empty hash' do
- expect(subject.test_report_for_latest_pipeline).to eq("{}")
+ it 'returns a hash with an empty "suites" array' do
+ expect(subject.test_report_for_pipeline).to eq({ suites: [] }.to_json)
+ end
end
- end
- context 'failed pipeline and failed test builds' do
- before do
- allow(subject).to receive(:fetch).with(failed_build_uri).and_return(test_report_for_build)
- end
+ context 'failed pipeline and failed test builds' do
+ before do
+ allow(subject).to receive(:fetch).with(failed_build_uri).and_return(test_report_for_build)
+ end
- it 'returns populated test list for suites' do
- actual = subject.test_report_for_latest_pipeline
- expected = {
- 'suites' => [test_report_for_build]
- }.to_json
+ it 'returns populated test list for suites' do
+ actual = subject.test_report_for_pipeline
+ expected = {
+ 'suites' => [test_report_for_build]
+ }.to_json
- expect(actual).to eq(expected)
+ expect(actual).to eq(expected)
+ end
end
- end
- context 'when receiving a server error' do
- let(:response) { instance_double('Net::HTTPResponse') }
- let(:error) { Net::HTTPServerException.new('server error', response) }
- let(:test_report_for_latest_pipeline) { subject.test_report_for_latest_pipeline }
+ context 'when receiving a server error' do
+ let(:response) { instance_double('Net::HTTPResponse') }
+ let(:error) { Net::HTTPServerException.new('server error', response) }
+ let(:test_report_for_pipeline) { subject.test_report_for_pipeline }
- before do
- allow(response).to receive(:code).and_return(response_code)
- allow(subject).to receive(:fetch).with(failed_build_uri).and_raise(error)
- end
+ before do
+ allow(response).to receive(:code).and_return(response_code)
+ allow(subject).to receive(:fetch).with(failed_build_uri).and_raise(error)
+ end
- context 'when response code is 404' do
- let(:response_code) { 404 }
+ context 'when response code is 404' do
+ let(:response_code) { 404 }
- it 'continues without the missing reports' do
- expected = { 'suites' => [] }.to_json
+ it 'continues without the missing reports' do
+ expected = { suites: [] }.to_json
- expect { test_report_for_latest_pipeline }.not_to raise_error
- expect(test_report_for_latest_pipeline).to eq(expected)
+ expect { test_report_for_pipeline }.not_to raise_error
+ expect(test_report_for_pipeline).to eq(expected)
+ end
end
- end
- context 'when response code is unexpected' do
- let(:response_code) { 500 }
+ context 'when response code is unexpected' do
+ let(:response_code) { 500 }
- it 'raises HTTPServerException' do
- expect { test_report_for_latest_pipeline }.to raise_error(error)
+ it 'raises HTTPServerException' do
+ expect { test_report_for_pipeline }.to raise_error(error)
+ end
end
end
end
end
+
+ context 'for latest pipeline' do
+ let(:failed_build_uri) { "#{latest_pipeline_url}/tests/suite.json?build_ids[]=#{failed_build_id}" }
+
+ subject { described_class.new(options.merge(pipeline_index: :latest)) }
+
+ it 'fetches builds from pipeline related to MR' do
+ expect(subject).to receive(:fetch).with(failed_build_uri).and_return(test_report_for_build)
+
+ subject.test_report_for_pipeline
+ end
+ end
end
end
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index 760b9bda541..78cc57b6c91 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Trigger, feature_category: :tooling do
'CI_COMMIT_SHA' => 'ci_commit_sha',
'CI_MERGE_REQUEST_PROJECT_ID' => 'ci_merge_request_project_id',
'CI_MERGE_REQUEST_IID' => 'ci_merge_request_iid',
- 'GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN' => 'bot-token',
+ 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'bot-token',
'CI_JOB_TOKEN' => 'job-token',
'GITLAB_USER_NAME' => 'gitlab_user_name',
'GITLAB_USER_LOGIN' => 'gitlab_user_login',
@@ -26,7 +26,7 @@ RSpec.describe Trigger, feature_category: :tooling do
end
let(:com_api_endpoint) { 'https://gitlab.com/api/v4' }
- let(:com_api_token) { env['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] }
+ let(:com_api_token) { env['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] }
let(:com_gitlab_client) { double('com_gitlab_client') }
let(:downstream_gitlab_client_endpoint) { com_api_endpoint }
@@ -114,25 +114,6 @@ RSpec.describe Trigger, feature_category: :tooling do
subject.invoke!
end
-
- context 'with downstream_job_name: "foo"' do
- let(:downstream_job) { Struct.new(:id, :name).new(42, 'foo') }
- let(:paginated_resources) { Struct.new(:auto_paginate).new([downstream_job]) }
-
- before do
- stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee")
- end
-
- it 'fetches the downstream job' do
- expect_run_trigger_with_params
- expect(downstream_gitlab_client).to receive(:pipeline_jobs)
- .with(downstream_project_path, stubbed_pipeline.id).and_return(paginated_resources)
- expect(Trigger::Job).to receive(:new)
- .with(downstream_project_path, downstream_job.id, downstream_gitlab_client)
-
- subject.invoke!(downstream_job_name: 'foo')
- end
- end
end
end
diff --git a/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb b/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb
index 8b45e8a64fc..7c53acbf168 100644
--- a/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb
+++ b/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEntity do
- let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
+ let(:stage) { build(:cycle_analytics_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json }
diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb
index ae992e478a6..4df542e3c98 100644
--- a/spec/serializers/ci/pipeline_entity_spec.rb
+++ b/spec/serializers/ci/pipeline_entity_spec.rb
@@ -36,11 +36,10 @@ RSpec.describe Ci::PipelineEntity do
expect(subject).to include :details
expect(subject[:details])
- .to include :duration, :finished_at, :name, :event_type_name
+ .to include :duration, :finished_at, :event_type_name
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label, :tooltip
expect(subject[:details][:event_type_name]).to eq('Merged result pipeline')
- expect(subject[:details][:name]).to eq('Merged result pipeline')
end
it 'contains flags' do
diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb
index 0390e232fd5..32269e5475b 100644
--- a/spec/serializers/codequality_degradation_entity_spec.rb
+++ b/spec/serializers/codequality_degradation_entity_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_a.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10")
+ expect(subject[:engine_name]).to eq('structure')
end
end
@@ -30,6 +31,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
+ expect(subject[:engine_name]).to eq('rubocop')
end
end
@@ -46,6 +48,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
+ expect(subject[:engine_name]).to eq('rubocop')
end
end
end
diff --git a/spec/serializers/import/github_realtime_repo_entity_spec.rb b/spec/serializers/import/github_realtime_repo_entity_spec.rb
new file mode 100644
index 00000000000..7f137366be2
--- /dev/null
+++ b/spec/serializers/import/github_realtime_repo_entity_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::GithubRealtimeRepoEntity, feature_category: :importers do
+ subject(:entity) { described_class.new(project) }
+
+ let(:import_state) { instance_double(ProjectImportState, failed?: false, in_progress?: true) }
+ let(:import_failures) { [instance_double(ImportFailure, exception_message: 'test error')] }
+ let(:project) do
+ instance_double(
+ Project,
+ id: 100500,
+ import_status: 'importing',
+ import_state: import_state,
+ import_failures: import_failures,
+ import_checksums: {}
+ )
+ end
+
+ it 'exposes correct attributes' do
+ data = entity.as_json
+
+ expect(data.keys).to contain_exactly(:id, :import_status, :stats)
+ expect(data[:id]).to eq project.id
+ expect(data[:import_status]).to eq project.import_status
+ end
+
+ context 'when import stats is failed' do
+ let(:import_state) { instance_double(ProjectImportState, failed?: true, in_progress?: false) }
+
+ it 'includes import_error' do
+ data = entity.as_json
+
+ expect(data.keys).to contain_exactly(:id, :import_status, :stats, :import_error)
+ expect(data[:import_error]).to eq 'test error'
+ end
+ end
+end
diff --git a/spec/serializers/import/github_realtime_repo_serializer_spec.rb b/spec/serializers/import/github_realtime_repo_serializer_spec.rb
new file mode 100644
index 00000000000..b656132e332
--- /dev/null
+++ b/spec/serializers/import/github_realtime_repo_serializer_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::GithubRealtimeRepoSerializer, feature_category: :importers do
+ subject(:serializer) { described_class.new }
+
+ it '.entity_class' do
+ expect(described_class.entity_class).to eq(Import::GithubRealtimeRepoEntity)
+ end
+
+ describe '#represent' do
+ let(:import_state) { instance_double(ProjectImportState, failed?: false, in_progress?: true) }
+ let(:project) do
+ instance_double(
+ Project,
+ id: 100500,
+ import_status: 'importing',
+ import_state: import_state
+ )
+ end
+
+ let(:expected_data) do
+ {
+ id: project.id,
+ import_status: 'importing',
+ stats: { fetched: {}, imported: {} }
+ }.deep_stringify_keys
+ end
+
+ context 'when a single object is being serialized' do
+ let(:resource) { project }
+
+ it 'serializes organization object' do
+ expect(serializer.represent(resource).as_json).to eq expected_data
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:count) { 3 }
+ let(:resource) { Array.new(count, project) }
+
+ it 'serializes array of organizations' do
+ expect(serializer.represent(resource).as_json).to all(eq(expected_data))
+ end
+ end
+ end
+end
diff --git a/spec/serializers/integrations/field_entity_spec.rb b/spec/serializers/integrations/field_entity_spec.rb
index 4212a1ee6a2..1ca1545c11a 100644
--- a/spec/serializers/integrations/field_entity_spec.rb
+++ b/spec/serializers/integrations/field_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::FieldEntity do
+RSpec.describe Integrations::FieldEntity, feature_category: :integrations do
let(:request) { EntityRequest.new(integration: integration) }
subject { described_class.new(field, request: request, integration: integration).as_json }
@@ -23,9 +23,9 @@ RSpec.describe Integrations::FieldEntity do
section: 'connection',
type: 'text',
name: 'username',
- title: 'Username or Email',
+ title: 'Username or email',
placeholder: nil,
- help: 'Use a username for server version and an email for cloud version.',
+ help: 'Username for the server version or an email for the cloud version',
required: true,
choices: nil,
value: 'jira_username',
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 9d53d8bb235..06d8523b2e7 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe IssueEntity do
before do
project.add_developer(member)
public_project.add_developer(member)
- Issues::MoveService.new(project: public_project, current_user: member).execute(issue, project)
+ Issues::MoveService.new(container: public_project, current_user: member).execute(issue, project)
end
context 'when user cannot read target project' do
@@ -97,7 +97,7 @@ RSpec.describe IssueEntity do
before do
Issues::DuplicateService
- .new(project: project, current_user: member)
+ .new(container: project, current_user: member)
.execute(issue, new_issue)
end
@@ -164,21 +164,57 @@ RSpec.describe IssueEntity do
it_behaves_like 'issuable entity current_user properties'
context 'when issue has email participants' do
+ let(:obfuscated_email) { 'an*****@e*****.c**' }
+ let(:email) { 'any@email.com' }
+
before do
- resource.issue_email_participants.create!(email: 'any@email.com')
+ resource.issue_email_participants.create!(email: email)
end
- context 'when issue is confidential' do
- it 'returns email participants' do
- resource.update!(confidential: true)
+ context 'with anonymous user' do
+ it 'returns obfuscated email participants email' do
+ request = double('request', current_user: nil)
- expect(subject[:issue_email_participants]).to match_array([{ email: "any@email.com" }])
+ response = described_class.new(resource, request: request).as_json
+ expect(response[:issue_email_participants]).to eq([{ email: obfuscated_email }])
end
end
- context 'when issue is not confidential' do
- it 'returns empty array' do
- expect(subject[:issue_email_participants]).to be_empty
+ context 'with signed in user' do
+ context 'when user has no role in project' do
+ it 'returns obfuscated email participants email' do
+ expect(subject[:issue_email_participants]).to eq([{ email: obfuscated_email }])
+ end
+ end
+
+ context 'when user has guest role in project' do
+ let(:member) { create(:user) }
+
+ before do
+ project.add_guest(member)
+ end
+
+ it 'returns obfuscated email participants email' do
+ request = double('request', current_user: member)
+
+ response = described_class.new(resource, request: request).as_json
+ expect(response[:issue_email_participants]).to eq([{ email: obfuscated_email }])
+ end
+ end
+
+ context 'when user has (at least) reporter role in project' do
+ let(:member) { create(:user) }
+
+ before do
+ project.add_reporter(member)
+ end
+
+ it 'returns full email participants email' do
+ request = double('request', current_user: member)
+
+ response = described_class.new(resource, request: request).as_json
+ expect(response[:issue_email_participants]).to eq([{ email: email }])
+ end
end
end
end
diff --git a/spec/serializers/merge_requests/pipeline_entity_spec.rb b/spec/serializers/merge_requests/pipeline_entity_spec.rb
index 414ce6653bc..acffa1e87a6 100644
--- a/spec/serializers/merge_requests/pipeline_entity_spec.rb
+++ b/spec/serializers/merge_requests/pipeline_entity_spec.rb
@@ -33,12 +33,11 @@ RSpec.describe MergeRequests::PipelineEntity do
)
expect(subject[:commit]).to include(:short_id, :commit_path)
expect(subject[:ref]).to include(:branch)
- expect(subject[:details]).to include(:artifacts, :name, :event_type_name, :status, :stages, :finished_at)
+ expect(subject[:details]).to include(:artifacts, :event_type_name, :status, :stages, :finished_at)
expect(subject[:details][:status]).to include(:icon, :favicon, :text, :label, :tooltip)
expect(subject[:flags]).to include(:merge_request_pipeline)
expect(subject[:details][:event_type_name]).to eq('Merged result pipeline')
- expect(subject[:details][:name]).to eq('Merged result pipeline')
end
it 'returns presented coverage' do
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index 764d55e8b4a..de05d2afd2b 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe PipelineDetailsEntity do
let(:pipeline) { create(:ci_empty_pipeline) }
before do
- create(:commit_status, pipeline: pipeline)
+ create(:ci_build, pipeline: pipeline)
end
it 'contains stages' do
@@ -182,6 +182,7 @@ RSpec.describe PipelineDetailsEntity do
expect(source_jobs[cross_project_pipeline.id][:name]).to eq('cross-project')
expect(source_jobs[child_pipeline.id][:name]).to eq('child')
+ expect(source_jobs[child_pipeline.id][:retried]).to eq false
end
end
end
diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb
index 94af9f1cbd8..6d292d18ae7 100644
--- a/spec/serializers/project_import_entity_spec.rb
+++ b/spec/serializers/project_import_entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectImportEntity do
+RSpec.describe ProjectImportEntity, feature_category: :importers do
include ImportHelper
let_it_be(:project) { create(:project, import_status: :started, import_source: 'namespace/project') }
@@ -10,6 +10,10 @@ RSpec.describe ProjectImportEntity do
let(:provider_url) { 'https://provider.com' }
let(:entity) { described_class.represent(project, provider_url: provider_url) }
+ before do
+ create(:import_failure, project: project)
+ end
+
describe '#as_json' do
subject { entity.as_json }
@@ -18,6 +22,19 @@ RSpec.describe ProjectImportEntity do
expect(subject[:import_status]).to eq(project.import_status)
expect(subject[:human_import_status_name]).to eq(project.human_import_status_name)
expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source]))
+ expect(subject[:import_error]).to eq(nil)
+ end
+
+ context 'when import is failed' do
+ let!(:last_import_failure) { create(:import_failure, project: project, exception_message: 'LAST ERROR') }
+
+ before do
+ project.import_state.fail_op!
+ end
+
+ it 'includes only the last import failure' do
+ expect(subject[:import_error]).to eq(last_import_failure.exception_message)
+ end
end
end
end
diff --git a/spec/services/achievements/create_service_spec.rb b/spec/services/achievements/create_service_spec.rb
index f62a45deb50..ac28a88572b 100644
--- a/spec/services/achievements/create_service_spec.rb
+++ b/spec/services/achievements/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::CreateService, feature_category: :users do
+RSpec.describe Achievements::CreateService, feature_category: :user_profile do
describe '#execute' do
let_it_be(:user) { create(:user) }
diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
index 24f0123ed3b..7bfae0cd9fc 100644
--- a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
+++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb
@@ -5,11 +5,14 @@ require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
+ let_it_be(:project_namespace) { project.project_namespace.reload }
- let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) }
+ let(:value_stream) { Analytics::CycleAnalytics::ValueStream.build_default_value_stream(project_namespace) }
let(:stages) { subject.payload[:stages] }
- subject { described_class.new(parent: project, current_user: user).execute }
+ subject do
+ described_class.new(parent: project_namespace, current_user: user, params: { value_stream: value_stream }).execute
+ end
before_all do
project.add_reporter(user)
diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb
index 1d079adc0be..4f8b90fcb4a 100644
--- a/spec/services/audit_event_service_spec.rb
+++ b/spec/services/audit_event_service_spec.rb
@@ -13,12 +13,12 @@ RSpec.describe AuditEventService, :with_license do
describe '#security_event' do
it 'creates an event and logs to a file' do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_id: project.id,
- entity_type: "Project",
- action: :destroy,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_id: project.id,
+ entity_type: "Project",
+ action: :destroy,
+ created_at: anything })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
end
@@ -33,15 +33,15 @@ RSpec.describe AuditEventService, :with_license do
target_id: 1
})
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_type: 'Project',
- entity_id: project.id,
- from: 'true',
- to: 'false',
- action: :create,
- target_id: 1,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_type: 'Project',
+ entity_id: project.id,
+ from: 'true',
+ to: 'false',
+ action: :create,
+ target_id: 1,
+ created_at: anything })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
@@ -58,12 +58,12 @@ RSpec.describe AuditEventService, :with_license do
it 'is overridden successfully' do
freeze_time do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_id: project.id,
- entity_type: "Project",
- action: :destroy,
- created_at: 3.weeks.ago)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_id: project.id,
+ entity_type: "Project",
+ action: :destroy,
+ created_at: 3.weeks.ago })
expect { service.security_event }.to change(AuditEvent, :count).by(1)
expect(AuditEvent.last.created_at).to eq(3.weeks.ago)
@@ -129,12 +129,12 @@ RSpec.describe AuditEventService, :with_license do
describe '#log_security_event_to_file' do
it 'logs security event to file' do
expect(service).to receive(:file_logger).and_return(logger)
- expect(logger).to receive(:info).with(author_id: user.id,
- author_name: user.name,
- entity_type: 'Project',
- entity_id: project.id,
- action: :destroy,
- created_at: anything)
+ expect(logger).to receive(:info).with({ author_id: user.id,
+ author_name: user.name,
+ entity_type: 'Project',
+ entity_id: project.id,
+ action: :destroy,
+ created_at: anything })
service.log_security_event_to_file
end
diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
index 11621055a47..da428bece20 100644
--- a/spec/services/authorized_project_update/project_access_changed_service_spec.rb
+++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
@@ -4,18 +4,11 @@ require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
describe '#execute' do
- it 'schedules the project IDs' do
- expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
- .with([[1], [2]])
-
- described_class.new([1, 2]).execute
- end
-
- it 'permits non-blocking operation' do
+ it 'executes projects_authorizations refresh' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
- described_class.new([1, 2]).execute(blocking: false)
+ described_class.new([1, 2]).execute
end
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
index 043b413acff..7584e44152e 100644
--- a/spec/services/auto_merge_service_spec.rb
+++ b/spec/services/auto_merge_service_spec.rb
@@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do
subject
end
- context 'when the head piipeline succeeded' do
+ context 'when the head pipeline succeeded' do
let(:pipeline_status) { :success }
it 'returns failed' do
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index 75f88e3989c..7f892cfe722 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -8,27 +8,27 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
let(:destination_group) { create(:group, path: 'destination1') }
let(:migrate_projects) { true }
let_it_be(:parent_group) { create(:group, path: 'parent-group') }
+ # note: destination_name and destination_slug are currently interchangable so we need to test for both possibilities
let(:params) do
[
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group1',
- destination_slug: 'destination group 1',
+ destination_slug: 'destination-group-1',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
-
},
{
source_type: 'group_entity',
source_full_path: 'full/path/to/group2',
- destination_slug: 'destination group 2',
+ destination_name: 'destination-group-2',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
},
{
source_type: 'project_entity',
source_full_path: 'full/path/to/project1',
- destination_slug: 'destination project 1',
+ destination_slug: 'destination-project-1',
destination_namespace: 'parent-group',
migrate_projects: migrate_projects
}
@@ -226,7 +226,12 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
expect(result).to be_a(ServiceResponse)
expect(result).to be_error
- expect(result.message).to eq("Validation failed: Source full path can't be blank")
+ expect(result.message).to eq("Validation failed: Source full path can't be blank, " \
+ "Source full path cannot start with a non-alphanumeric character except " \
+ "for periods or underscores, can contain only alphanumeric characters, " \
+ "forward slashes, periods, and underscores, cannot end with " \
+ "a period or forward slash, and has a relative path structure " \
+ "with no http protocol chars or leading or trailing forward slashes")
end
describe '#user-role' do
@@ -267,56 +272,188 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
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'
- }
- ]
+ 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
- it 'defines access_level from destination_namespace' do
- destination_group.add_developer(user)
- subject.execute
+ 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
- expect_snowplow_event(
- category: 'BulkImports::CreateService',
- action: 'create',
- label: 'import_access_level',
- user: user,
- extra: { user_role: 'Developer', import_type: 'bulk_import_group' }
- )
+ 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
- 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'
- }
- ]
+ describe '.validate_destination_full_path' do
+ context 'when the source_type is a group' do
+ context 'when the provided destination_slug already exists in the destination_namespace' do
+ let_it_be(:existing_subgroup) { create(:group, path: 'existing-subgroup', parent_id: parent_group.id ) }
+ let_it_be(:existing_subgroup_2) { create(:group, path: 'existing-subgroup_2', parent_id: parent_group.id ) }
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_subgroup.path,
+ destination_namespace: parent_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'parent-group/existing-subgroup' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug conflicts with an existing top-level namespace' do
+ let_it_be(:existing_top_level_group) { create(:group, path: 'top-level-group') }
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_top_level_group.path,
+ destination_namespace: '',
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'top-level-group' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug does not conflict with an existing top-level namespace' do
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: 'new-group',
+ destination_namespace: parent_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns success ServiceResponse' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+ end
end
- it 'defines access_level as owner' do
- subject.execute
+ context 'when the source_type is a project' do
+ context 'when the provided destination_slug already exists in the destination_namespace' do
+ let_it_be(:existing_group) { create(:group, path: 'existing-group' ) }
+ let_it_be(:existing_project) { create(:project, path: 'existing-project', parent_id: existing_group.id ) }
+ let(:params) do
+ [
+ {
+ source_type: 'project_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: existing_project.path,
+ destination_namespace: existing_group.path,
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
- expect_snowplow_event(
- category: 'BulkImports::CreateService',
- action: 'create',
- label: 'import_access_level',
- user: user,
- extra: { user_role: 'Owner', import_type: 'bulk_import_group' }
- )
+ it 'returns ServiceResponse with an error message' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Import aborted as 'existing-group/existing-project' already exists. " \
+ "Change the destination and try again."
+ )
+ end
+ end
+
+ context 'when the destination_slug does not conflict with an existing project' do
+ let_it_be(:existing_group) { create(:group, path: 'existing-group' ) }
+ let(:params) do
+ [
+ {
+ source_type: 'project_entity',
+ source_full_path: 'full/path/to/source',
+ destination_slug: 'new-project',
+ destination_namespace: 'existing-group',
+ migrate_projects: migrate_projects
+ }
+ ]
+ end
+
+ it 'returns success ServiceResponse' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+ end
end
end
end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
index 4c261ece504..c9b4439202a 100644
--- a/spec/services/chat_names/authorize_user_service_spec.rb
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatNames::AuthorizeUserService, feature_category: :users do
+RSpec.describe ChatNames::AuthorizeUserService, feature_category: :user_profile do
describe '#execute' do
let(:result) { subject.execute }
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
index 359ea0699e4..3fb9d092ae7 100644
--- a/spec/services/ci/archive_trace_service_spec.rb
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ArchiveTraceService, '#execute' do
+RSpec.describe Ci::ArchiveTraceService, '#execute', feature_category: :continuous_integration do
subject { described_class.new.execute(job, worker_name: Ci::ArchiveTraceWorker.name) }
context 'when job is finished' do
@@ -192,4 +192,69 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
expect(job.trace_metadata.archival_attempts).to eq(1)
end
end
+
+ describe '#batch_execute' do
+ subject { described_class.new.batch_execute(worker_name: Ci::ArchiveTraceWorker.name) }
+
+ let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+ let_it_be_with_reload(:job2) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+
+ it 'archives multiple traces' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_exist
+ expect(job2.reload.job_artifacts_trace).to be_exist
+ end
+
+ it 'processes traces independently' do
+ allow_next_instance_of(Gitlab::Ci::Trace) do |instance|
+ orig_method = instance.method(:archive!)
+ allow(instance).to receive(:archive!) do
+ raise('Unexpected error') if instance.job.id == job.id
+
+ orig_method.call
+ end
+ end
+
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ expect(job2.reload.job_artifacts_trace).to be_exist
+ end
+
+ context 'when timeout is reached' do
+ before do
+ stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
+ end
+
+ it 'stops executing traces' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ end
+ end
+
+ context 'when loop limit is reached' do
+ before do
+ stub_const("#{described_class}::LOOP_LIMIT", -1)
+ end
+
+ it 'skips archiving' do
+ expect(job.trace).not_to receive(:archive!)
+
+ subject
+ end
+
+ it 'stops executing traces' do
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: Ci::ArchiveTraceWorker.name,
+ message: "Loop limit reached.",
+ job_id: job.id)
+
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb
new file mode 100644
index 00000000000..f2eaa8d31b4
--- /dev/null
+++ b/spec/services/ci/components/fetch_service_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do
+ let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { user }
+ let_it_be(:current_host) { Gitlab.config.gitlab.host }
+
+ let(:service) do
+ described_class.new(address: address, current_user: current_user)
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute', :aggregate_failures do
+ subject(:result) { service.execute }
+
+ shared_examples 'an external component' do
+ shared_examples 'component address' do
+ context 'when content exists' do
+ let(:sha) { project.commit(version).id }
+
+ let(:content) do
+ <<~COMPONENT
+ job:
+ script: echo
+ COMPONENT
+ end
+
+ before do
+ stub_project_blob(sha, component_yaml_path, content)
+ end
+
+ it 'returns the content' do
+ expect(result).to be_success
+ expect(result.payload[:content]).to eq(content)
+ end
+ end
+
+ context 'when content does not exist' do
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+ end
+
+ context 'when user does not have permissions to read the code' do
+ let(:version) { 'master' }
+ let(:current_user) { create(:user) }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:not_allowed)
+ end
+ end
+
+ context 'when version is a branch name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.default_branch }
+ end
+ end
+
+ context 'when version is a tag name' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.name }
+ end
+ end
+
+ context 'when version is a commit sha' do
+ it_behaves_like 'component address' do
+ let(:version) { project.repository.tags.first.id }
+ end
+ end
+
+ context 'when version is not provided' do
+ let(:version) { nil }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:component_path) { 'unknown/component' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:content_not_found)
+ end
+ end
+
+ context 'when host is different than the current instance host' do
+ let(:current_host) { 'another-host.com' }
+ let(:version) { '1.0' }
+
+ it 'returns an error' do
+ expect(result).to be_error
+ expect(result.reason).to eq(:unsupported_path)
+ end
+ end
+ end
+
+ context 'when address points to an external component' do
+ let(:address) { "#{current_host}/#{component_path}@#{version}" }
+
+ context 'when component path is the full path to a project' do
+ let(:component_path) { project.full_path }
+ let(:component_yaml_path) { 'template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-component" }
+ let(:component_yaml_path) { 'my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+
+ context 'when component path points to a nested directory in a project' do
+ let(:component_path) { "#{project.full_path}/my-dir/my-component" }
+ let(:component_yaml_path) { 'my-dir/my-component/template.yml' }
+
+ it_behaves_like 'an external component'
+ end
+ end
+ end
+
+ def stub_project_blob(ref, path, content)
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content)
+ 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 fd978bffacb..7b576339c61 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -890,23 +890,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute', feature_category
end
end
end
-
- context 'with :ci_limit_complete_hierarchy_size disabled' do
- before do
- stub_feature_flags(ci_limit_complete_hierarchy_size: false)
- end
-
- 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
- subject
-
- expect(bridge.reload).to be_success
- end
- end
end
end
end
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index 711002e28af..47e9e5994ef 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -132,6 +132,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do
expect(new_artifact).to be_public_accessibility
end
+ it 'logs the created artifact and metadata' do
+ expect(Gitlab::Ci::Artifacts::Logger)
+ .to receive(:log_created)
+ .with(an_instance_of(Ci::JobArtifact)).twice
+
+ subject
+ end
+
context 'when accessibility level passed as private' do
before do
params.merge!('accessibility' => 'private')
diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
index dd10c0df374..457be67c1ea 100644
--- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state do
+RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state,
+feature_category: :build_artifacts do
include ExclusiveLeaseHelpers
let(:service) { described_class.new }
diff --git a/spec/services/ci/job_token_scope/add_project_service_spec.rb b/spec/services/ci/job_token_scope/add_project_service_spec.rb
index bf7df3a5595..e6674ee384f 100644
--- a/spec/services/ci/job_token_scope/add_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/add_project_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::JobTokenScope::AddProjectService do
+RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuous_integration do
let(:service) { described_class.new(project, current_user) }
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -21,6 +21,8 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target projects' do
+ let(:resulting_direction) { result.payload.fetch(:project_link)&.direction }
+
before do
project.add_maintainer(current_user)
target_project.add_developer(current_user)
@@ -34,6 +36,26 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
end
it_behaves_like 'adds project'
+
+ it 'creates an outbound link by default' do
+ expect(resulting_direction).to eq('outbound')
+ end
+
+ context 'when direction is specified' do
+ subject(:result) { service.execute(target_project, direction: direction) }
+
+ context 'when the direction is outbound' do
+ let(:direction) { :outbound }
+
+ specify { expect(resulting_direction).to eq('outbound') }
+ end
+
+ context 'when the direction is inbound' do
+ let(:direction) { :inbound }
+
+ specify { expect(resulting_direction).to eq('inbound') }
+ end
+ end
end
end
diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
index c3f9081cbd8..5b39f8908f2 100644
--- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb
+++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::JobTokenScope::RemoveProjectService do
+RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :continuous_integration do
let(:service) { described_class.new(project, current_user) }
let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
@@ -23,7 +23,7 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService do
end
describe '#execute' do
- subject(:result) { service.execute(target_project) }
+ subject(:result) { service.execute(target_project, :outbound) }
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target project' do
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 5b865914d1b..e2bbdefef7f 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -2,19 +2,21 @@
require 'spec_helper'
-RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_caching do
+RSpec.describe Ci::ListConfigVariablesService,
+:use_clean_rails_memory_store_caching, feature_category: :pipeline_authoring do
include ReactiveCachingHelpers
let(:ci_config) { {} }
let(:files) { { '.gitlab-ci.yml' => YAML.dump(ci_config) } }
let(:project) { create(:project, :custom_repo, :auto_devops_disabled, files: files) }
let(:user) { project.creator }
- let(:sha) { project.default_branch }
+ let(:ref) { project.default_branch }
+ let(:sha) { project.commit(ref).sha }
let(:service) { described_class.new(project, user) }
- subject(:result) { service.execute(sha) }
+ subject(:result) { service.execute(ref) }
- context 'when sending a valid sha' do
+ context 'when sending a valid ref' do
let(:ci_config) do
{
variables: {
@@ -109,8 +111,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
end
end
- context 'when sending an invalid sha' do
- let(:sha) { 'invalid-sha' }
+ context 'when sending an invalid ref' do
+ let(:ref) { 'invalid-ref' }
let(:ci_config) { nil }
before do
diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
index 7b3af33ac72..f720375f05c 100644
--- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb
+++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ParseDotenvArtifactService do
+RSpec.describe Ci::ParseDotenvArtifactService, feature_category: :build_artifacts do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -223,6 +223,18 @@ RSpec.describe Ci::ParseDotenvArtifactService do
end
end
+ context 'when blob is encoded in UTF-16 LE' do
+ let(:blob) { File.read(Rails.root.join('spec/fixtures/build_artifacts/dotenv_utf16_le.txt')) }
+
+ it 'parses the dotenv data' do
+ subject
+
+ expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly(
+ hash_including('key' => 'MY_ENV_VAR', 'value' => 'true'),
+ hash_including('key' => 'TEST2', 'value' => 'false'))
+ end
+ end
+
context 'when more than limitated variables are specified in dotenv' do
let(:blob) do
StringIO.new.tap do |s|
diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb
new file mode 100644
index 00000000000..402bc2faa81
--- /dev/null
+++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineCreation::CancelRedundantPipelinesService, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:prev_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:new_commit) { create(:commit, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
+
+ let(:service) { described_class.new(pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: prev_pipeline)
+ create(:ci_build, :interruptible, :success, pipeline: prev_pipeline)
+ create(:ci_build, :created, pipeline: prev_pipeline)
+
+ create(:ci_build, :interruptible, pipeline: pipeline)
+ end
+
+ describe '#execute!' do
+ subject(:execute) { service.execute }
+
+ context 'when build statuses are set up correctly' do
+ it 'has builds of all statuses' do
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+
+ context 'when auto-cancel is enabled' do
+ before do
+ project.update!(auto_cancel_pending_pipelines: 'enabled')
+ end
+
+ it 'cancels only previous interruptible builds' do
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+
+ it 'logs canceled pipelines' do
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ execute
+
+ expect(Gitlab::AppLogger).to have_received(:info).with(
+ class: described_class.name,
+ message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}",
+ canceled_pipeline_id: prev_pipeline.id,
+ canceled_by_pipeline_id: pipeline.id,
+ canceled_by_pipeline_source: pipeline.source
+ )
+ end
+
+ it 'cancels the builds with 2 queries to avoid query timeout' do
+ second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/
+ recorder = ActiveRecord::QueryRecorder.new { execute }
+ second_query = recorder.occurrences.keys.filter { |occ| occ =~ second_query_regex }
+
+ expect(second_query).to be_one
+ end
+
+ context 'when the previous pipeline has a child pipeline' do
+ let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) }
+
+ context 'with another nested child pipeline' do
+ let(:another_child_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
+ end
+
+ it 'cancels all nested child pipeline builds' do
+ expect(build_statuses(another_child_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(another_child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+ end
+
+ context 'when started after pipeline was finished' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ prev_pipeline.update!(status: "success")
+ end
+
+ it 'cancels child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled')
+ end
+ end
+
+ context 'when the child pipeline has interruptible running jobs' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ end
+
+ it 'cancels all child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+
+ context 'when the child pipeline includes completed interruptible jobs' do
+ before do
+ create(:ci_build, :interruptible, :failed, pipeline: child_pipeline)
+ create(:ci_build, :interruptible, :success, pipeline: child_pipeline)
+ end
+
+ it 'cancels all child pipeline builds with a cancelable_status' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success')
+ end
+ end
+ end
+
+ context 'when the child pipeline has started non-interruptible job' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ # non-interruptible started
+ create(:ci_build, :success, pipeline: child_pipeline)
+ end
+
+ it 'does not cancel any child pipeline builds' do
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
+ end
+ end
+
+ context 'when the child pipeline has non-interruptible non-started job' do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
+ end
+
+ not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES
+ context 'when the jobs are cancelable' do
+ cancelable_not_started_statuses =
+ Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES)
+ cancelable_not_started_statuses.each do |status|
+ it "cancels all child pipeline builds when build status #{status} included" do
+ # non-interruptible but non-started
+ create(:ci_build, status.to_sym, pipeline: child_pipeline)
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
+ end
+ end
+ end
+
+ context 'when the jobs are not cancelable' do
+ not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES
+ not_cancelable_not_started_statuses.each do |status|
+ it "does not cancel child pipeline builds when build status #{status} included" do
+ # non-interruptible but non-started
+ create(:ci_build, status.to_sym, pipeline: child_pipeline)
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('running', status)
+
+ execute
+
+ expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the pipeline is a child pipeline' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
+ let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) }
+
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
+ create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
+ end
+
+ it 'does not cancel any builds' do
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
+
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
+ end
+ end
+
+ context 'when the previous pipeline source is webide' do
+ let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) }
+
+ it 'does not cancel builds of the previous pipeline' do
+ execute
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+
+ it 'does not cancel future pipelines' do
+ expect(prev_pipeline.id).to be < pipeline.id
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+
+ described_class.new(prev_pipeline).execute
+
+ expect(build_statuses(pipeline.reload)).to contain_exactly('pending')
+ end
+ end
+
+ context 'when auto-cancel is disabled' do
+ before do
+ project.update!(auto_cancel_pending_pipelines: 'disabled')
+ end
+
+ it 'does not cancel any build' do
+ subject
+
+ expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
+ expect(build_statuses(pipeline)).to contain_exactly('pending')
+ end
+ end
+ end
+
+ private
+
+ def build_statuses(pipeline)
+ pipeline.builds.pluck(:status)
+ end
+end
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
index 2f094583f1a..8896d8ace30 100644
--- a/spec/services/ci/pipeline_schedule_service_spec.rb
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::PipelineScheduleService do
+RSpec.describe Ci::PipelineScheduleService, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
new file mode 100644
index 00000000000..838f49f6dea
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuous_integration do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_reporter(reporter)
+ end
+
+ describe "execute" do
+ context 'when user does not have permission' do
+ subject(:service) { described_class.new(pipeline_schedule, reporter, {}) }
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(_('The current user is not authorized to update the pipeline schedule'))
+ end
+ end
+
+ context 'when user has permission' do
+ let(:params) do
+ {
+ description: 'updated_desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *'
+ }
+ end
+
+ subject(:service) { described_class.new(pipeline_schedule, user, params) }
+
+ it 'updates database values with passed params' do
+ expect { service.execute }
+ .to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
+ .and change { pipeline_schedule.ref }.from('master').to('patch-x')
+ .and change { pipeline_schedule.active }.from(true).to(false)
+ .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ expect(result.payload.description).to eq('updated_desc')
+ end
+
+ context 'when schedule update fails' do
+ subject(:service) { described_class.new(pipeline_schedule, user, {}) }
+
+ before do
+ allow(pipeline_schedule).to receive(:update).and_return(false)
+
+ errors = ActiveModel::Errors.new(pipeline_schedule)
+ errors.add(:base, 'An error occurred')
+ allow(pipeline_schedule).to receive(:errors).and_return(errors)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to match_array(['An error occurred'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index f40f5cc5a62..9183df359b4 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -3,795 +3,830 @@
require 'spec_helper'
module Ci
- RSpec.describe RegisterJobService do
+ RSpec.describe RegisterJobService, feature_category: :continuous_integration do
let_it_be(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:shared_runner) { create(:ci_runner, :instance) }
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:shared_runner) { create(:ci_runner, :instance) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
describe '#execute' do
- subject { described_class.new(shared_runner).execute }
+ subject(:execute) { described_class.new(runner, runner_machine).execute }
+
+ context 'with runner_machine specified' do
+ let(:runner) { project_runner }
+ let!(:runner_machine) { create(:ci_runner_machine, runner: project_runner) }
- context 'checks database loadbalancing stickiness' do
before do
- project.update!(shared_runners_enabled: false)
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["linux"])
end
- it 'result is valid if replica did caught-up', :aggregate_failures do
- expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
- .with(:runner, shared_runner.id) { true }
+ it 'sets runner_machine on job' do
+ expect { execute }.to change { pending_job.reload.runner_machine }.from(nil).to(runner_machine)
- expect(subject).to be_valid
- expect(subject.build).to be_nil
- expect(subject.build_json).to be_nil
+ expect(execute.build).to eq(pending_job)
end
+ end
- it 'result is invalid if replica did not caught-up', :aggregate_failures do
- expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
- .with(:runner, shared_runner.id) { false }
+ context 'with no runner machine' do
+ let(:runner_machine) { nil }
- expect(subject).not_to be_valid
- expect(subject.build).to be_nil
- expect(subject.build_json).to be_nil
- end
- end
+ context 'checks database loadbalancing stickiness' do
+ let(:runner) { shared_runner }
- shared_examples 'handles runner assignment' do
- context 'runner follow tag list' do
- it "picks build with the same tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- specific_runner.update!(tag_list: ["linux"])
- expect(execute(specific_runner)).to eq(pending_job)
+ before do
+ project.update!(shared_runners_enabled: false)
end
- it "does not pick build with different tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- specific_runner.update!(tag_list: ["win32"])
- expect(execute(specific_runner)).to be_falsey
- end
+ it 'result is valid if replica did caught-up', :aggregate_failures do
+ expect(ApplicationRecord.sticking).to receive(:all_caught_up?).with(:runner, runner.id) { true }
- it "picks build without tag" do
- expect(execute(specific_runner)).to eq(pending_job)
+ expect(execute).to be_valid
+ expect(execute.build).to be_nil
+ expect(execute.build_json).to be_nil
end
- it "does not pick build with tag" do
- pending_job.update!(tag_list: ["linux"])
- pending_job.reload
- pending_job.create_queuing_entry!
- expect(execute(specific_runner)).to be_falsey
- end
+ it 'result is invalid if replica did not caught-up', :aggregate_failures do
+ expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
+ .with(:runner, shared_runner.id) { false }
- it "pick build without tag" do
- specific_runner.update!(tag_list: ["win32"])
- expect(execute(specific_runner)).to eq(pending_job)
+ expect(subject).not_to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
end
- context 'deleted projects' do
- before do
- project.update!(pending_delete: true)
- end
+ shared_examples 'handles runner assignment' do
+ context 'runner follow tag list' do
+ it "picks build with the same tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["linux"])
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
- context 'for shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
+ it "does not pick build with different tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ project_runner.update!(tag_list: ["win32"])
+ expect(build_on(project_runner)).to be_falsey
end
- it 'does not pick a build' do
- expect(execute(shared_runner)).to be_nil
+ it "picks build without tag" do
+ expect(build_on(project_runner)).to eq(pending_job)
end
- end
- context 'for specific runner' do
- it 'does not pick a build' do
- expect(execute(specific_runner)).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job.queuing_entry).to be_nil
+ it "does not pick build with tag" do
+ pending_job.update!(tag_list: ["linux"])
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ expect(build_on(project_runner)).to be_falsey
end
- end
- end
- context 'allow shared runners' do
- before do
- project.update!(shared_runners_enabled: true)
- pipeline.reload
- pending_job.reload
- pending_job.create_queuing_entry!
+ it "pick build without tag" do
+ project_runner.update!(tag_list: ["win32"])
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- context 'when build owner has been blocked' do
- let(:user) { create(:user, :blocked) }
-
+ context 'deleted projects' do
before do
- pending_job.update!(user: user)
+ project.update!(pending_delete: true)
end
- it 'does not pick the build and drops the build' do
- expect(execute(shared_runner)).to be_falsey
+ context 'for shared runners' do
+ before do
+ project.update!(shared_runners_enabled: true)
+ end
- expect(pending_job.reload).to be_user_blocked
+ it 'does not pick a build' do
+ expect(build_on(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for project runner' do
+ it 'does not pick a build' do
+ expect(build_on(project_runner)).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
+ end
end
end
- context 'for multiple builds' do
- let!(:project2) { create :project, shared_runners_enabled: true }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :project, shared_runners_enabled: true }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
-
- it 'picks builds one-by-one' do
- expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
-
- expect(execute(shared_runner)).to eq(build1_project1)
- end
-
- context 'when using fair scheduling' do
- context 'when all builds are pending' do
- it 'prefers projects without builds first' do
- # it gets for one build from each of the projects
- expect(execute(shared_runner)).to eq(build1_project1)
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
-
- # then it gets a second build from each of the projects
- expect(execute(shared_runner)).to eq(build2_project1)
- expect(execute(shared_runner)).to eq(build2_project2)
-
- # in the end the third build
- expect(execute(shared_runner)).to eq(build3_project1)
- end
+ context 'allow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: true)
+ pipeline.reload
+ pending_job.reload
+ pending_job.create_queuing_entry!
+ end
+
+ context 'when build owner has been blocked' do
+ let(:user) { create(:user, :blocked) }
+
+ before do
+ pending_job.update!(user: user)
end
- context 'when some builds transition to success' do
- it 'equalises number of running builds' do
- # after finishing the first build for project 1, get a second build from the same project
- expect(execute(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(execute(shared_runner)).to eq(build2_project1)
+ it 'does not pick the build and drops the build' do
+ expect(build_on(shared_runner)).to be_falsey
- expect(execute(shared_runner)).to eq(build1_project2)
- build1_project2.reload.success
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
- expect(execute(shared_runner)).to eq(build3_project1)
- end
+ expect(pending_job.reload).to be_user_blocked
end
end
- context 'when using DEFCON mode that disables fair scheduling' do
- before do
- stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
- end
-
- context 'when all builds are pending' do
- it 'returns builds in order of creation (FIFO)' do
- # it gets for one build from each of the projects
- expect(execute(shared_runner)).to eq(build1_project1)
- expect(execute(shared_runner)).to eq(build2_project1)
- expect(execute(shared_runner)).to eq(build3_project1)
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
+ context 'for multiple builds' do
+ let!(:project2) { create :project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) }
+
+ it 'picks builds one-by-one' do
+ expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
+
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ end
+
+ context 'when using fair scheduling' do
+ context 'when all builds are pending' do
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ end
+ end
+
+ context 'when some builds transition to success' do
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ build1_project2.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ end
end
end
- context 'when some builds transition to success' do
- it 'returns builds in order of creation (FIFO)' do
- expect(execute(shared_runner)).to eq(build1_project1)
- build1_project1.reload.success
- expect(execute(shared_runner)).to eq(build2_project1)
+ context 'when using DEFCON mode that disables fair scheduling' do
+ before do
+ stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true)
+ end
+
+ context 'when all builds are pending' do
+ it 'returns builds in order of creation (FIFO)' do
+ # it gets for one build from each of the projects
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ expect(build_on(shared_runner)).to eq(build2_project1)
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ end
+ end
- expect(execute(shared_runner)).to eq(build3_project1)
- build2_project1.reload.success
- expect(execute(shared_runner)).to eq(build1_project2)
- expect(execute(shared_runner)).to eq(build2_project2)
- expect(execute(shared_runner)).to eq(build1_project3)
+ context 'when some builds transition to success' do
+ it 'returns builds in order of creation (FIFO)' do
+ expect(build_on(shared_runner)).to eq(build1_project1)
+ build1_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build2_project1)
+
+ expect(build_on(shared_runner)).to eq(build3_project1)
+ build2_project1.reload.success
+ expect(build_on(shared_runner)).to eq(build1_project2)
+ expect(build_on(shared_runner)).to eq(build2_project2)
+ expect(build_on(shared_runner)).to eq(build1_project3)
+ end
end
end
end
- end
- context 'shared runner' do
- let(:response) { described_class.new(shared_runner).execute }
- let(:build) { response.build }
+ context 'shared runner' do
+ let(:response) { described_class.new(shared_runner, nil).execute }
+ let(:build) { response.build }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(shared_runner) }
- it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
- end
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) }
+ end
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
+ end
end
- end
- context 'disallow shared runners' do
- before do
- project.update!(shared_runners_enabled: false)
- end
+ context 'disallow shared runners' do
+ before do
+ project.update!(shared_runners_enabled: false)
+ end
- context 'shared runner' do
- let(:build) { execute(shared_runner) }
+ context 'shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'specific runner' do
- let(:build) { execute(specific_runner) }
+ context 'project runner' do
+ let(:build) { build_on(project_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(specific_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(project_runner) }
+ end
end
- end
- context 'disallow when builds are disabled' do
- before do
- project.update!(shared_runners_enabled: true, group_runners_enabled: true)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ context 'disallow when builds are disabled' do
+ before do
+ project.update!(shared_runners_enabled: true, group_runners_enabled: true)
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'and uses shared runner' do
- let(:build) { execute(shared_runner) }
+ context 'and uses shared runner' do
+ let(:build) { build_on(shared_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses group runner' do
- let(:build) { execute(group_runner) }
+ context 'and uses group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
- end
+ it { expect(build).to be_nil }
+ end
- context 'and uses project runner' do
- let(:build) { execute(specific_runner) }
+ context 'and uses project runner' do
+ let(:build) { build_on(project_runner) }
- it 'does not pick a build' do
- expect(build).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job.queuing_entry).to be_nil
+ it 'does not pick a build' do
+ expect(build).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job.queuing_entry).to be_nil
+ end
end
end
- end
- context 'allow group runners' do
- before do
- project.update!(group_runners_enabled: true)
- end
+ context 'allow group runners' do
+ before do
+ project.update!(group_runners_enabled: true)
+ end
- context 'for multiple builds' do
- let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline2) { create(:ci_pipeline, project: project2) }
- let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
- let!(:pipeline3) { create(:ci_pipeline, project: project3) }
+ context 'for multiple builds' do
+ let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline2) { create(:ci_pipeline, project: project2) }
+ let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project3) }
- let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) }
- # these shouldn't influence the scheduling
- let!(:unrelated_group) { create(:group) }
- let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
- let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
- let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
- let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
+ # these shouldn't influence the scheduling
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
+ let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
+ let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) }
+ let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
- it 'does not consider builds from other group runners' do
- queue = ::Ci::Queue::BuildQueueService.new(group_runner)
+ it 'does not consider builds from other group runners' do
+ queue = ::Ci::Queue::BuildQueueService.new(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 6
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 6
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 5
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 5
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 4
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 4
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 3
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 3
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 2
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 2
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 1
- execute(group_runner)
+ expect(queue.builds_for_group_runner.size).to eq 1
+ build_on(group_runner)
- expect(queue.builds_for_group_runner.size).to eq 0
- expect(execute(group_runner)).to be_nil
+ expect(queue.builds_for_group_runner.size).to eq 0
+ expect(build_on(group_runner)).to be_nil
+ end
end
- end
- context 'group runner' do
- let(:build) { execute(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_kind_of(Build) }
- it { expect(build).to be_valid }
- it { expect(build).to be_running }
- it { expect(build.runner).to eq(group_runner) }
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(group_runner) }
+ end
end
- end
- context 'disallow group runners' do
- before do
- project.update!(group_runners_enabled: false)
+ context 'disallow group runners' do
+ before do
+ project.update!(group_runners_enabled: false)
- pending_job.reload.create_queuing_entry!
- end
+ pending_job.reload.create_queuing_entry!
+ end
- context 'group runner' do
- let(:build) { execute(group_runner) }
+ context 'group runner' do
+ let(:build) { build_on(group_runner) }
- it { expect(build).to be_nil }
+ it { expect(build).to be_nil }
+ end
end
- end
- context 'when first build is stalled' do
- before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
- allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
- .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
- end
+ context 'when first build is stalled' do
+ before do
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!)
+ .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError)
+ end
- subject { described_class.new(specific_runner).execute }
+ subject { described_class.new(project_runner, nil).execute }
- context 'with multiple builds are in queue' do
- let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'with multiple builds are in queue' do
+ let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
- end
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id))
+ end
- it "receives second build from the queue" do
- expect(subject).to be_valid
- expect(subject.build).to eq(other_build)
+ it "receives second build from the queue" do
+ expect(subject).to be_valid
+ expect(subject.build).to eq(other_build)
+ end
end
- end
- context 'when single build is in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return(Ci::Build.where(id: pending_job).pluck(:id))
- end
+ context 'when single build is in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return(Ci::Build.where(id: pending_job).pluck(:id))
+ end
- it "does not receive any valid result" do
- expect(subject).not_to be_valid
+ it "does not receive any valid result" do
+ expect(subject).not_to be_valid
+ end
end
- end
- context 'when there is no build in queue' do
- before do
- allow_any_instance_of(::Ci::Queue::BuildQueueService)
- .to receive(:execute)
- .and_return([])
- end
+ context 'when there is no build in queue' do
+ before do
+ allow_any_instance_of(::Ci::Queue::BuildQueueService)
+ .to receive(:execute)
+ .and_return([])
+ end
- it "does not receive builds but result is valid" do
- expect(subject).to be_valid
- expect(subject.build).to be_nil
+ it "does not receive builds but result is valid" do
+ expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ end
end
end
- end
- context 'when access_level of runner is not_protected' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ context 'when access_level of runner is not_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
end
- end
- context 'when access_level of runner is ref_protected' do
- let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
+ context 'when access_level of runner is ref_protected' do
+ let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
- context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
- it 'picks the job' do
- expect(execute(specific_runner)).to eq(pending_job)
+ it 'picks the job' do
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- end
- context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- it 'does not pick the job' do
- expect(execute(specific_runner)).to be_nil
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
+ end
end
- end
- context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
- before do
- pending_job.update_attribute(:protected, nil)
- end
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
- it 'does not pick the job' do
- expect(execute(specific_runner)).to be_nil
+ it 'does not pick the job' do
+ expect(build_on(project_runner)).to be_nil
+ end
end
end
- end
- context 'runner feature set is verified' do
- let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
+ context 'runner feature set is verified' do
+ let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
- subject { execute(specific_runner, params) }
+ subject { build_on(project_runner, params: params) }
- context 'when feature is missing by runner' do
- let(:params) { {} }
+ context 'when feature is missing by runner' do
+ let(:params) { {} }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_runner_unsupported
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_runner_unsupported
+ end
end
- end
- context 'when feature is supported by runner' do
- let(:params) do
- { info: { features: { upload_multiple_artifacts: true } } }
- end
+ context 'when feature is supported by runner' do
+ let(:params) do
+ { info: { features: { upload_multiple_artifacts: true } } }
+ end
- it 'does pick job' do
- expect(subject).not_to be_nil
+ it 'does pick job' do
+ expect(subject).not_to be_nil
+ end
end
end
- end
-
- context 'when "dependencies" keyword is specified' do
- let!(:pre_stage_job) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
- end
- let!(:pending_job) do
- create(:ci_build, :pending, :queued,
- pipeline: pipeline, stage_idx: 1,
- options: { script: ["bash"], dependencies: dependencies })
- end
+ context 'when "dependencies" keyword is specified' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- let(:dependencies) { %w[test] }
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued,
+ pipeline: pipeline, stage_idx: 1,
+ options: { script: ["bash"], dependencies: dependencies })
+ end
- subject { execute(specific_runner) }
+ let(:dependencies) { %w[test] }
- it 'picks a build with a dependency' do
- picked_build = execute(specific_runner)
+ subject { build_on(project_runner) }
- expect(picked_build).to be_present
- end
+ it 'picks a build with a dependency' do
+ picked_build = build_on(project_runner)
- context 'when there are multiple dependencies with artifacts' do
- let!(:pre_stage_job_second) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
+ expect(picked_build).to be_present
end
- let(:dependencies) { %w[test deploy] }
-
- it 'logs build artifacts size' do
- execute(specific_runner)
-
- artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
- job.job_artifacts_archive.size
+ context 'when there are multiple dependencies with artifacts' do
+ let!(:pre_stage_job_second) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0)
end
- expect(artifacts_size).to eq 107464 * 2
- expect(Gitlab::ApplicationContext.current).to include({
- 'meta.artifacts_dependencies_size' => artifacts_size,
- 'meta.artifacts_dependencies_count' => 2
- })
- end
- end
+ let(:dependencies) { %w[test deploy] }
- shared_examples 'not pick' do
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
- expect(pending_job.reload).to be_failed
- expect(pending_job).to be_missing_dependency_failure
- end
- end
+ it 'logs build artifacts size' do
+ build_on(project_runner)
- shared_examples 'validation is active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job|
+ job.job_artifacts_archive.size
+ end
- it { is_expected.to eq(pending_job) }
+ expect(artifacts_size).to eq 107464 * 2
+ expect(Gitlab::ApplicationContext.current).to include({
+ 'meta.artifacts_dependencies_size' => artifacts_size,
+ 'meta.artifacts_dependencies_count' => 2
+ })
+ end
end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ shared_examples 'not pick' do
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_missing_dependency_failure
+ end
+ end
- context 'when the pipeline is locked' do
- before do
- pipeline.artifacts_locked!
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
end
it { is_expected.to eq(pending_job) }
end
- context 'when the pipeline is unlocked' do
- before do
- pipeline.unlocked!
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
end
- it_behaves_like 'not pick'
+ context 'when the pipeline is locked' do
+ before do
+ pipeline.artifacts_locked!
+ end
+
+ it { is_expected.to eq(pending_job) }
+ end
+
+ context 'when the pipeline is unlocked' do
+ before do
+ pipeline.unlocked!
+ end
+
+ it_behaves_like 'not pick'
+ end
end
- end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
- it_behaves_like 'not pick'
- end
+ it_behaves_like 'not pick'
+ end
- context 'when job object is staled' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ context 'when job object is staled' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- before do
- pipeline.unlocked!
+ before do
+ pipeline.unlocked!
- allow_next_instance_of(Ci::Build) do |build|
- expect(build).to receive(:drop!)
- .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ allow_next_instance_of(Ci::Build) do |build|
+ expect(build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ end
end
- end
- it 'does not drop nor pick' do
- expect(subject).to be_nil
+ it 'does not drop nor pick' do
+ expect(subject).to be_nil
+ end
end
end
- end
- shared_examples 'validation is not active' do
- context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- it { expect(subject).to eq(pending_job) }
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- context 'when artifacts of depended job has been expired' do
- let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0)
+ end
- it { expect(subject).to eq(pending_job) }
- end
+ it { expect(subject).to eq(pending_job) }
+ end
- context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
- it { expect(subject).to eq(pending_job) }
+ it { expect(subject).to eq(pending_job) }
+ end
end
- end
- it_behaves_like 'validation is active'
- end
+ it_behaves_like 'validation is active'
+ end
- context 'when build is degenerated' do
- let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
+ context 'when build is degenerated' do
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does not pick the build and drops the build' do
- expect(subject).to be_nil
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_archived_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_archived_failure
+ end
end
- end
- context 'when build has data integrity problem' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build has data integrity problem' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- pending_job.update_columns(options: "string")
- end
+ before do
+ pending_job.update_columns(options: "string")
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs both failures' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .twice
- .and_call_original
+ it 'does drop the build and logs both failures' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .twice
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_data_integrity_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_data_integrity_failure
+ end
end
- end
- context 'when build fails to be run!' do
- let!(:pending_job) do
- create(:ci_build, :pending, :queued, pipeline: pipeline)
- end
+ context 'when build fails to be run!' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
+ end
- before do
- expect_any_instance_of(Ci::Build).to receive(:run!)
- .and_raise(RuntimeError, 'scheduler error')
- end
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!)
+ .and_raise(RuntimeError, 'scheduler error')
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'does drop the build and logs failure' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(anything, a_hash_including(build_id: pending_job.id))
- .once
- .and_call_original
+ it 'does drop the build and logs failure' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(anything, a_hash_including(build_id: pending_job.id))
+ .once
+ .and_call_original
- expect(subject).to be_nil
+ expect(subject).to be_nil
- pending_job.reload
- expect(pending_job).to be_failed
- expect(pending_job).to be_scheduler_failure
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_scheduler_failure
+ end
end
- end
- context 'when an exception is raised during a persistent ref creation' do
- before do
- allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
- allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
- end
+ context 'when an exception is raised during a persistent ref creation' do
+ before do
+ allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
+ allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError }
+ end
- subject { execute(specific_runner, {}) }
+ subject { build_on(project_runner) }
- it 'picks the build' do
- expect(subject).to eq(pending_job)
+ it 'picks the build' do
+ expect(subject).to eq(pending_job)
- pending_job.reload
- expect(pending_job).to be_running
- end
- end
-
- context 'when only some builds can be matched by runner' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
-
- before do
- # create additional matching and non-matching jobs
- create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
- create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
+ pending_job.reload
+ expect(pending_job).to be_running
+ end
end
- it 'observes queue size of only matching jobs' do
- # pending_job + 2 x matching ones
- expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, 3)
+ context 'when only some builds can be matched by runner' do
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) }
- expect(execute(specific_runner)).to eq(pending_job)
- end
+ before do
+ # create additional matching and non-matching jobs
+ create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching])
+ create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching])
+ end
- it 'observes queue processing time by the runner type' do
- expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, anything)
+ it 'observes queue size of only matching jobs' do
+ # pending_job + 2 x matching ones
+ expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, 3)
- expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
- .to receive(:observe)
- .with({ runner_type: specific_runner.runner_type }, anything)
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
- expect(execute(specific_runner)).to eq(pending_job)
- end
- end
+ it 'observes queue processing time by the runner type' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- context 'when ci_register_job_temporary_lock is enabled' do
- before do
- stub_feature_flags(ci_register_job_temporary_lock: true)
+ expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds)
+ .to receive(:observe)
+ .with({ runner_type: project_runner.runner_type }, anything)
- allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ expect(build_on(project_runner)).to eq(pending_job)
+ end
end
- context 'when a build is temporarily locked' do
- let(:service) { described_class.new(specific_runner) }
-
+ context 'when ci_register_job_temporary_lock is enabled' do
before do
- service.send(:acquire_temporary_lock, pending_job.id)
- end
-
- it 'skips this build and marks queue as invalid' do
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration)
- expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :build_temporary_locked)
+ stub_feature_flags(ci_register_job_temporary_lock: true)
- expect(service.execute).not_to be_valid
+ allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
end
- context 'when there is another build in queue' do
- let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+ context 'when a build is temporarily locked' do
+ let(:service) { described_class.new(project_runner, nil) }
- it 'skips this build and picks another build' do
+ before do
+ service.send(:acquire_temporary_lock, pending_job.id)
+ end
+
+ it 'skips this build and marks queue as invalid' do
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
- .with(operation: :queue_iteration).twice
+ .with(operation: :queue_iteration)
expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
.with(operation: :build_temporary_locked)
- result = service.execute
+ expect(service.execute).not_to be_valid
+ end
+
+ context 'when there is another build in queue' do
+ let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
+
+ it 'skips this build and picks another build' do
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :queue_iteration).twice
+ expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment)
+ .with(operation: :build_temporary_locked)
- expect(result.build).to eq(next_pending_job)
- expect(result).to be_valid
+ result = service.execute
+
+ expect(result.build).to eq(next_pending_job)
+ expect(result).to be_valid
+ end
end
end
end
end
- end
-
- context 'when using pending builds table' do
- include_examples 'handles runner assignment'
- context 'when a conflicting data is stored in denormalized table' do
- let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
- let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
+ context 'when using pending builds table' do
+ include_examples 'handles runner assignment'
- before do
- pending_job.update_column(:status, :running)
- end
+ context 'when a conflicting data is stored in denormalized table' do
+ let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) }
- it 'removes queuing entry upon build assignment attempt' do
- expect(pending_job.reload).to be_running
- expect(pending_job.queuing_entry).to be_present
+ before do
+ pending_job.update_column(:status, :running)
+ end
- result = described_class.new(specific_runner).execute
+ it 'removes queuing entry upon build assignment attempt' do
+ expect(pending_job.reload).to be_running
+ expect(pending_job.queuing_entry).to be_present
- expect(result).not_to be_valid
- expect(pending_job.reload.queuing_entry).not_to be_present
+ expect(execute).not_to be_valid
+ expect(pending_job.reload.queuing_entry).not_to be_present
+ end
end
end
end
@@ -807,11 +842,11 @@ module Ci
# Stub tested metrics
allow(Gitlab::Ci::Queue::Metrics)
.to receive(:attempt_counter)
- .and_return(attempt_counter)
+ .and_return(attempt_counter)
allow(Gitlab::Ci::Queue::Metrics)
.to receive(:job_queue_duration_seconds)
- .and_return(job_queue_duration_seconds)
+ .and_return(job_queue_duration_seconds)
project.update!(shared_runners_enabled: true)
pending_job.update!(created_at: current_time - 3600, queued_at: current_time - 1800)
@@ -822,7 +857,7 @@ module Ci
allow(job_queue_duration_seconds).to receive(:observe)
expect(attempt_counter).to receive(:increment)
- execute(runner)
+ build_on(runner)
end
end
@@ -834,7 +869,7 @@ module Ci
jobs_running_for_project: expected_jobs_running_for_project_first_job,
shard: expected_shard }, 1800)
- execute(runner)
+ build_on(runner)
end
context 'when project already has running jobs' do
@@ -854,7 +889,7 @@ module Ci
jobs_running_for_project: expected_jobs_running_for_project_third_job,
shard: expected_shard }, 1800)
- execute(runner)
+ build_on(runner)
end
end
end
@@ -913,12 +948,12 @@ module Ci
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).not_to receive(:observe)
- execute(runner)
+ build_on(runner)
end
end
end
- context 'when specific runner is used' do
+ context 'when project runner is used' do
let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w(tag1 metrics_shard::shard_tag tag2)) }
let(:expected_shared_runner) { false }
let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD }
@@ -933,12 +968,12 @@ module Ci
it 'present sets runner session configuration in the build' do
runner_session_params = { session: { 'url' => 'https://example.com' } }
- expect(execute(specific_runner, runner_session_params).runner_session.attributes)
+ expect(build_on(project_runner, params: runner_session_params).runner_session.attributes)
.to include(runner_session_params[:session])
end
it 'not present it does not configure the runner session' do
- expect(execute(specific_runner).runner_session).to be_nil
+ expect(build_on(project_runner).runner_session).to be_nil
end
end
@@ -954,7 +989,7 @@ module Ci
it 'returns 409 conflict' do
expect(Ci::Build.pending.unstarted.count).to eq 3
- result = described_class.new(specific_runner).execute
+ result = described_class.new(project_runner, nil).execute
expect(result).not_to be_valid
expect(result.build).to be_nil
@@ -962,8 +997,8 @@ module Ci
end
end
- def execute(runner, params = {})
- described_class.new(runner).execute(params).build
+ def build_on(runner, runner_machine: nil, params: {})
+ described_class.new(runner, runner_machine).execute(params).build
end
end
end
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index c3d80f2cb56..10acf032b1a 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::RetryJobService do
+RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do
using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
@@ -27,6 +27,22 @@ RSpec.describe Ci::RetryJobService do
project.add_reporter(reporter)
end
+ shared_context 'retryable bridge' do
+ let_it_be(:downstream_project) { create(:project, :repository) }
+
+ let_it_be_with_refind(:job) do
+ create(:ci_bridge, :success,
+ pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
+ )
+ end
+
+ let_it_be(:job_to_clone) { job }
+
+ before do
+ job.update!(retried: false)
+ end
+ end
+
shared_context 'retryable build' do
let_it_be_with_reload(:job) do
create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
@@ -102,6 +118,14 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_examples_for 'does not retry the job' do
+ it 'returns :not_retryable and :unprocessable_entity' do
+ expect(subject.message).to be('Job cannot be retried')
+ expect(subject.payload[:reason]).to eq(:not_retryable)
+ expect(subject.payload[:job]).to eq(job)
+ end
+ end
+
shared_examples_for 'retries the job' do
it_behaves_like 'clones the job'
@@ -189,6 +213,20 @@ RSpec.describe Ci::RetryJobService do
expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError)
end
+ context 'when the job to be cloned is a bridge' do
+ include_context 'retryable bridge'
+
+ it_behaves_like 'clones the job'
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ it 'does not give variables to the new bridge' do
+ expect { new_job }.not_to raise_error
+ end
+ end
+ end
+
context 'when the job to be cloned is a build' do
include_context 'retryable build'
@@ -287,7 +325,33 @@ RSpec.describe Ci::RetryJobService do
subject { service.execute(job) }
+ context 'when the job to be retried is a bridge' do
+ context 'and it is not retryable' do
+ let_it_be(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth) }
+
+ it_behaves_like 'does not retry the job'
+ end
+
+ include_context 'retryable bridge'
+
+ it_behaves_like 'retries the job'
+
+ context 'when given variables' do
+ let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+
+ it 'does not give variables to the new bridge' do
+ expect { new_job }.not_to raise_error
+ end
+ end
+ end
+
context 'when the job to be retried is a build' do
+ context 'and it is not retryable' do
+ let_it_be(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) }
+
+ it_behaves_like 'does not retry the job'
+ end
+
include_context 'retryable build'
it_behaves_like 'retries the job'
diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb
new file mode 100644
index 00000000000..673bf3ef90e
--- /dev/null
+++ b/spec/services/ci/runners/create_runner_service_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do
+ subject(:execute) { described_class.new(user: current_user, type: type, params: params).execute }
+
+ let(:runner) { execute.payload[:runner] }
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_admin_user) { create(:user) }
+ let_it_be(:anonymous) { nil }
+
+ shared_context 'when admin user' do
+ let(:current_user) { admin }
+
+ before do
+ allow(current_user).to receive(:can?).with(:create_instance_runners).and_return true
+ end
+ end
+
+ shared_examples 'it can create a runner' do
+ it 'creates a runner of the specified type' do
+ expect(runner.runner_type).to eq expected_type
+ end
+
+ context 'with default params provided' do
+ let(:args) do
+ {}
+ end
+
+ before do
+ params.merge!(args)
+ end
+
+ it { is_expected.to be_success }
+
+ it 'uses default values when none are provided' do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.persisted?).to be_truthy
+ expect(runner.run_untagged).to be true
+ expect(runner.active).to be true
+ expect(runner.creator).to be current_user
+ expect(runner.authenticated_user_registration_type?).to be_truthy
+ expect(runner.runner_type).to eq 'instance_type'
+ end
+ end
+
+ context 'with non-default params provided' do
+ let(:args) do
+ {
+ description: 'some description',
+ maintenance_note: 'a note',
+ paused: true,
+ tag_list: %w[tag1 tag2],
+ access_level: 'ref_protected',
+ locked: true,
+ maximum_timeout: 600,
+ run_untagged: false
+ }
+ end
+
+ before do
+ params.merge!(args)
+ end
+
+ it { is_expected.to be_success }
+
+ it 'creates runner with specified values', :aggregate_failures do
+ expect(runner).to be_an_instance_of(::Ci::Runner)
+ expect(runner.description).to eq 'some description'
+ expect(runner.maintenance_note).to eq 'a note'
+ expect(runner.active).to eq !args[:paused]
+ expect(runner.locked).to eq args[:locked]
+ expect(runner.run_untagged).to eq args[:run_untagged]
+ expect(runner.tags).to contain_exactly(
+ an_object_having_attributes(name: 'tag1'),
+ an_object_having_attributes(name: 'tag2')
+ )
+ expect(runner.access_level).to eq args[:access_level]
+ expect(runner.maximum_timeout).to eq args[:maximum_timeout]
+
+ expect(runner.authenticated_user_registration_type?).to be_truthy
+ expect(runner.runner_type).to eq 'instance_type'
+ end
+ end
+ end
+
+ shared_examples 'it cannot create a runner' do
+ it 'runner payload is nil' do
+ expect(runner).to be nil
+ end
+
+ it { is_expected.to be_error }
+ end
+
+ shared_examples 'it can return an error' do
+ let(:group) { create(:group) }
+ let(:runner_double) { Ci::Runner.new }
+
+ context 'when the runner fails to save' do
+ before do
+ allow(Ci::Runner).to receive(:new).and_return runner_double
+ end
+
+ it_behaves_like 'it cannot create a runner'
+
+ it 'returns error message' do
+ expect(execute.errors).not_to be_empty
+ end
+ end
+ end
+
+ context 'with type param set to nil' do
+ let(:expected_type) { 'instance_type' }
+ let(:type) { nil }
+ let(:params) { {} }
+
+ it_behaves_like 'it cannot create a runner' do
+ let(:current_user) { anonymous }
+ end
+
+ it_behaves_like 'it cannot create a runner' do
+ let(:current_user) { non_admin_user }
+ end
+
+ it_behaves_like 'it can create a runner' do
+ include_context 'when admin user'
+ end
+
+ it_behaves_like 'it can return an error' do
+ include_context 'when admin user'
+ end
+ end
+end
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 d2a7e87b2d5..e62cb1ec3e3 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
@@ -53,14 +53,14 @@ RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category:
end
context 'with existing ci_runner_version record' do
- let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) }
+ let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :unavailable) }
it 'updates ci_runner_versions record', :aggregate_failures do
expect do
expect(execute).to be_success
expect(execute.http_status).to eq :ok
expect(execute.payload).to eq({ upgrade_status: 'recommended' })
- end.to change { runner_version.reload.status }.from('not_available').to('recommended')
+ end.to change { runner_version.reload.status }.from('unavailable').to('recommended')
end
end
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 39082b5c0f4..8d7e97e5ea8 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
@@ -9,7 +9,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') }
let_it_be(:runner_version_14_0_1) do
- create(:ci_runner_version, version: '14.0.1', status: :not_available)
+ create(:ci_runner_version, version: '14.0.1', status: :unavailable)
end
context 'with RunnerUpgradeCheck recommending 14.0.2' do
@@ -23,15 +23,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with runner with new version' do
let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') }
- let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) }
let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
+ let!(:runner_version_14_0_0) do
+ create(:ci_runner_version, version: '14.0.0', status: :unavailable)
+ end
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
.and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended])
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
.with('14.0.2')
- .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
.once
end
@@ -43,9 +45,9 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
.and_call_original
expect { execute }
- .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended')
- .and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended')
- .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available')
+ .to change { runner_version_14_0_0.reload.status }.from('unavailable').to('recommended')
+ .and change { runner_version_14_0_1.reload.status }.from('unavailable').to('recommended')
+ .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -57,17 +59,19 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
context 'with orphan ci_runner_version' do
- let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) }
+ let!(:runner_version_14_0_2) do
+ create(:ci_runner_version, version: '14.0.2', status: :unavailable)
+ end
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
- .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
end
it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
expect { execute }
- .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil)
- .and not_change { runner_version_14_0_1.reload.status }.from('not_available')
+ .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('unavailable').to(nil)
+ .and not_change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -81,11 +85,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
context 'with no runner version changes' do
before do
allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
- .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available])
+ .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :unavailable])
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -103,7 +107,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
it 'makes no changes to ci_runner_versions', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
@@ -121,7 +125,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute'
end
it 'does not modify ci_runner_versions entries', :aggregate_failures do
- expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available')
+ expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')
expect(execute).to be_success
expect(execute.payload).to eq({
diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb
index 47d399cb19a..c67040e45eb 100644
--- a/spec/services/ci/runners/register_runner_service_spec.rb
+++ b/spec/services/ci/runners/register_runner_service_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor
expect(runner.run_untagged).to be true
expect(runner.active).to be true
expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to start_with(::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX)
expect(runner).to be_instance_type
end
diff --git a/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb
new file mode 100644
index 00000000000..456dbcebb84
--- /dev/null
+++ b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Runners::StaleMachinesCleanupService, feature_category: :runner_fleet do
+ let(:service) { described_class.new }
+ let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
+
+ subject(:response) { service.execute }
+
+ context 'with no stale runner machines' do
+ it 'does not clean any runner machines and returns :success status' do
+ expect do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: false })
+ end.not_to change { Ci::RunnerMachine.count }.from(1)
+ end
+ end
+
+ context 'with some stale runner machines' do
+ before do
+ create(:ci_runner_machine, :stale)
+ create(:ci_runner_machine, :stale, contacted_at: nil)
+ end
+
+ it 'only leaves non-stale runners' do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: true })
+ expect(Ci::RunnerMachine.all).to contain_exactly(runner_machine3)
+ end
+
+ context 'with more stale runners than MAX_DELETIONS' do
+ before do
+ stub_const("#{described_class}::MAX_DELETIONS", 1)
+ end
+
+ it 'only leaves non-stale runners' do
+ expect do
+ expect(response).to be_success
+ expect(response.payload).to match({ deleted_machines: true })
+ end.to change { Ci::RunnerMachine.count }.by(-Ci::Runners::StaleMachinesCleanupService::MAX_DELETIONS)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index d3f537a1aa0..dd26339831c 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -277,7 +277,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
end
end
- context 'when updating specific runners' do
+ context 'when updating project runners' do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
it_behaves_like 'matching build'
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 90a86e7ae59..f8ecff20728 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::UpdateBuildStateService do
+RSpec.describe Ci::UpdateBuildStateService, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
index fa38bc202e7..51c054ddc98 100644
--- a/spec/services/clusters/agents/refresh_authorization_service_spec.rb
+++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe Clusters::Agents::RefreshAuthorizationService do
+RSpec.describe Clusters::Agents::RefreshAuthorizationService, feature_category: :kubernetes_management do
describe '#execute' do
let_it_be(:root_ancestor) { create(:group) }
let_it_be(:removed_group) { create(:group, parent: root_ancestor) }
let_it_be(:modified_group) { create(:group, parent: root_ancestor) }
- let_it_be(:added_group) { create(:group, parent: root_ancestor) }
+ let_it_be(:added_group) { create(:group, path: 'group-path-with-UPPERCASE', parent: root_ancestor) }
let_it_be(:removed_project) { create(:project, namespace: root_ancestor) }
let_it_be(:modified_project) { create(:project, namespace: root_ancestor) }
- let_it_be(:added_project) { create(:project, namespace: root_ancestor) }
+ let_it_be(:added_project) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let(:project) { create(:project, namespace: root_ancestor) }
let(:agent) { create(:cluster_agent, project: project) }
@@ -22,11 +22,13 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do
ci_access: {
groups: [
{ id: added_group.full_path, default_namespace: 'default' },
- { id: modified_group.full_path, default_namespace: 'new-namespace' }
+ # Uppercase path verifies case-insensitive matching.
+ { id: modified_group.full_path.upcase, default_namespace: 'new-namespace' }
],
projects: [
{ id: added_project.full_path, default_namespace: 'default' },
- { id: modified_project.full_path, default_namespace: 'new-namespace' }
+ # Uppercase path verifies case-insensitive matching.
+ { id: modified_project.full_path.upcase, default_namespace: 'new-namespace' }
]
}
}.deep_stringify_keys
diff --git a/spec/services/concerns/rate_limited_service_spec.rb b/spec/services/concerns/rate_limited_service_spec.rb
index 04007e8e75a..d913cd17067 100644
--- a/spec/services/concerns/rate_limited_service_spec.rb
+++ b/spec/services/concerns/rate_limited_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe RateLimitedService do
let(:key) { :issues_create }
- let(:scope) { [:project, :current_user] }
+ let(:scope) { [:container, :current_user] }
let(:opts) { { scope: scope, users_allowlist: -> { [User.support_bot.username] } } }
let(:rate_limiter) { ::Gitlab::ApplicationRateLimiter }
@@ -39,7 +39,7 @@ RSpec.describe RateLimitedService do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
- let(:service) { instance_double(Issues::CreateService, project: project, current_user: current_user) }
+ let(:service) { instance_double(Issues::CreateService, container: project, current_user: current_user) }
let(:evaluated_scope) { [project, current_user] }
let(:evaluated_opts) { { scope: evaluated_scope, users_allowlist: %w[support-bot] } }
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index e60954a19ed..b969bd76053 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -319,7 +319,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:category) { described_class.to_s }
let(:action) { :push }
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(:property) { 'project_action' }
end
@@ -346,7 +345,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:category) { described_class.to_s }
let(:action) { :push }
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(:property) { 'project_action' }
end
diff --git a/spec/services/export_csv/base_service_spec.rb b/spec/services/export_csv/base_service_spec.rb
new file mode 100644
index 00000000000..e2b4d4829af
--- /dev/null
+++ b/spec/services/export_csv/base_service_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExportCsv::BaseService, feature_category: :importers do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:relation) { Issue.all }
+ let_it_be(:resource_parent) { issue.project }
+
+ subject { described_class.new(relation, resource_parent) }
+
+ describe '#email' do
+ it 'raises NotImplementedError' do
+ user = create(:user)
+
+ expect { subject.email(user) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#header_to_value_hash' do
+ it 'raises NotImplementedError' do
+ expect { subject.send(:header_to_value_hash) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#associations_to_preload' do
+ it 'return []' do
+ expect(subject.send(:associations_to_preload)).to eq([])
+ end
+ end
+end
diff --git a/spec/services/export_csv/map_export_fields_service_spec.rb b/spec/services/export_csv/map_export_fields_service_spec.rb
new file mode 100644
index 00000000000..0060baf30e4
--- /dev/null
+++ b/spec/services/export_csv/map_export_fields_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ExportCsv::MapExportFieldsService, feature_category: :team_planning do
+ let(:selected_fields) { ['Title', 'Author username', 'state'] }
+ let(:invalid_fields) { ['Title', 'Author Username', 'State', 'Invalid Field', 'Other Field'] }
+ let(:data) do
+ {
+ 'Requirement ID' => '1',
+ 'Title' => 'foo',
+ 'Description' => 'bar',
+ 'Author' => 'root',
+ 'Author Username' => 'admin',
+ 'Created At (UTC)' => '2023-02-01 15:16:35',
+ 'State' => 'opened',
+ 'State Updated At (UTC)' => '2023-02-01 15:16:35'
+ }
+ end
+
+ describe '#execute' do
+ it 'returns a hash with selected fields only' do
+ result = described_class.new(selected_fields, data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to match_array(selected_fields.map(&:titleize))
+ end
+
+ context 'when the fields collection is empty' do
+ it 'returns a hash with all fields' do
+ result = described_class.new([], data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to match_array(data.keys)
+ end
+ end
+
+ context 'when fields collection includes invalid fields' do
+ it 'returns a hash with valid selected fields only' do
+ result = described_class.new(invalid_fields, data).execute
+
+ expect(result).to be_a(Hash)
+ expect(result.keys).to eq(selected_fields.map(&:titleize))
+ end
+ end
+ end
+
+ describe '#invalid_fields' do
+ it 'returns an array containing invalid fields' do
+ result = described_class.new(invalid_fields, data).invalid_fields
+
+ expect(result).to match_array(['Invalid Field', 'Other Field'])
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index 878a5c4ccf0..b076b2d51ef 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Git::WikiPushService, services: true do
+RSpec.describe Git::WikiPushService, services: true, feature_category: :wiki do
include RepoHelpers
let_it_be(:current_user) { create(:user) }
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 ef77958fa60..e5f06824b9f 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
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GoogleCloud::FetchGoogleIpListService, :use_clean_rails_memory_store_caching,
-:clean_gitlab_redis_rate_limiting, feature_category: :continuous_integration do
+:clean_gitlab_redis_rate_limiting, feature_category: :build_artifacts do
include StubRequests
let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) }
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0425ba3e631..84794b5f6f8 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::CreateService, '#execute' do
+RSpec.describe Groups::CreateService, '#execute', feature_category: :subgroups do
let!(:user) { create(:user) }
let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
@@ -78,10 +78,6 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
- it 'adds an onboarding progress record' do
- expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1)
- end
-
context 'with before_commit callback' do
it_behaves_like 'has sync-ed traversal_ids'
end
@@ -107,10 +103,6 @@ RSpec.describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
- it 'does not add an onboarding progress record' do
- expect { subject }.not_to change(Onboarding::Progress, :count).from(0)
- end
-
it_behaves_like 'has sync-ed traversal_ids'
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 2791203f395..7c3710aeeb2 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::DestroyService do
+RSpec.describe Groups::DestroyService, feature_category: :subgroups do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
index 03de7175edd..a570c28cf8b 100644
--- a/spec/services/groups/group_links/destroy_service_spec.rb
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -70,8 +70,8 @@ RSpec.describe Groups::GroupLinks::DestroyService, '#execute' do
it 'updates project authorization once per group' do
expect(GroupGroupLink).to receive(:delete).and_call_original
- expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
- expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
+ expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once
+ expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once
subject.execute(links)
end
diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
index 8565299b9b7..a28a552746f 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures do
+RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures, feature_category: :importers do
let(:remote_url) { 'https://external.file.path/file.tar.gz' }
let(:params) { { remote_import_url: remote_url } }
@@ -40,23 +40,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile,
end
end
- context 'when import_project_from_remote_file_s3 is enabled' do
- before do
- stub_feature_flags(import_project_from_remote_file_s3: true)
- end
-
- context 'when the HTTP request fail to recover the headers' do
- it 'adds the error message' do
- expect(Gitlab::HTTP)
- .to receive(:head)
- .and_raise(StandardError, 'request invalid')
+ context 'when the HTTP request fails to recover the headers' do
+ it 'adds the error message' do
+ expect(Gitlab::HTTP)
+ .to receive(:head)
+ .and_raise(StandardError, 'request invalid')
- expect(subject).not_to be_valid
- expect(subject.errors.full_messages)
- .to include('Failed to retrive headers: request invalid')
- end
+ expect(subject).not_to be_valid
+ expect(subject.errors.full_messages)
+ .to include('Failed to retrive headers: request invalid')
end
+ end
+ context 'when request is not from an S3 server' do
it 'validates the remote content-length' do
stub_headers_for(remote_url, { 'content-length' => 11.gigabytes })
@@ -72,57 +68,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile,
expect(subject.errors.full_messages)
.to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)")
end
-
- context 'when trying to import from AWS S3' do
- it 'adds an error suggesting to use `projects/remote-import-s3`' do
- stub_headers_for(
- remote_url,
- 'Server' => 'AmazonS3',
- 'x-amz-request-id' => 'some-id'
- )
-
- expect(subject).not_to be_valid
- expect(subject.errors.full_messages)
- .to include('To import from AWS S3 use `projects/remote-import-s3`')
- end
- end
end
- context 'when import_project_from_remote_file_s3 is disabled' do
- before do
- stub_feature_flags(import_project_from_remote_file_s3: false)
- end
-
- context 'when trying to import from AWS S3' do
- it 'does not validate the remote content-length or content-type' do
- stub_headers_for(
- remote_url,
- 'Server' => 'AmazonS3',
- 'x-amz-request-id' => 'some-id',
- 'content-length' => 11.gigabytes,
- 'content-type' => 'unknown'
- )
-
- expect(subject).to be_valid
- end
- end
-
- context 'when NOT trying to import from AWS S3' do
- it 'validates content-length and content-type' do
- stub_headers_for(
- remote_url,
- 'Server' => 'NOT AWS S3',
- 'content-length' => 11.gigabytes,
- 'content-type' => 'unknown'
- )
-
- expect(subject).not_to be_valid
-
- expect(subject.errors.full_messages)
- .to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)")
- expect(subject.errors.full_messages)
- .to include('Content length is too big (should be at most 10 GB)')
- end
+ context 'when request is from an S3 server' do
+ it 'does not validate the remote content-length or content-type' do
+ stub_headers_for(
+ remote_url,
+ 'Server' => 'AmazonS3',
+ 'x-amz-request-id' => 'some-id',
+ 'content-length' => 11.gigabytes,
+ 'content-type' => 'unknown'
+ )
+
+ expect(subject).to be_valid
end
end
end
diff --git a/spec/services/import_csv/base_service_spec.rb b/spec/services/import_csv/base_service_spec.rb
new file mode 100644
index 00000000000..0c0ed40ff4d
--- /dev/null
+++ b/spec/services/import_csv/base_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ImportCsv::BaseService, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:csv_io) { double }
+
+ subject { described_class.new(user, project, csv_io) }
+
+ shared_examples 'abstract method' do |method, args|
+ it "raises NotImplemented error when #{method} is called" do
+ if args
+ expect { subject.send(method, args) }.to raise_error(NotImplementedError)
+ else
+ expect { subject.send(method) }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+
+ it_behaves_like 'abstract method', :email_results_to_user
+ it_behaves_like 'abstract method', :attributes_for, "any"
+ it_behaves_like 'abstract method', :validate_headers_presence!, "any"
+ it_behaves_like 'abstract method', :create_object_class
+
+ describe '#detect_col_sep' do
+ context 'when header contains invalid separators' do
+ it 'raises error' do
+ header = 'Name&email'
+
+ expect { subject.send(:detect_col_sep, header) }.to raise_error(CSV::MalformedCSVError)
+ end
+ end
+
+ context 'when header is valid' do
+ shared_examples 'header with valid separators' do
+ let(:header) { "Name#{separator}email" }
+
+ it 'returns separator value' do
+ expect(subject.send(:detect_col_sep, header)).to eq(separator)
+ end
+ end
+
+ context 'with ; as separator' do
+ let(:separator) { ';' }
+
+ it_behaves_like 'header with valid separators'
+ end
+
+ context 'with \t as separator' do
+ let(:separator) { "\t" }
+
+ it_behaves_like 'header with valid separators'
+ end
+
+ context 'with , as separator' do
+ let(:separator) { ',' }
+
+ it_behaves_like 'header with valid separators'
+ end
+ end
+ end
+end
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 a3810879c65..fa5f4c64a43 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
occurred_at: Time.current,
action: 'new comment',
promoted_from_note: comment,
- timeline_event_tag_names: ['start time', 'end time']
+ timeline_event_tag_names: ['start time', 'end time', 'Impact mitigated']
}
end
@@ -180,11 +180,11 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it 'matches the two tags on the event and creates on project' do
result = execute.payload[:timeline_event]
- expect(result.timeline_event_tags.count).to eq(2)
- expect(result.timeline_event_tags.by_names(['Start time', 'End time']).pluck_names)
- .to match_array(['Start time', 'End time'])
+ expect(result.timeline_event_tags.count).to eq(3)
+ expect(result.timeline_event_tags.by_names(['Start time', 'End time', 'Impact mitigated']).pluck_names)
+ .to match_array(['Start time', 'End time', 'Impact mitigated'])
expect(project.incident_management_timeline_event_tags.pluck_names)
- .to include('Start time', 'End time')
+ .to include('Start time', 'End time', 'Impact mitigated')
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 ff802109715..ebaa4dde7a2 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -201,20 +201,22 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_catego
{
note: 'Updated note',
occurred_at: occurred_at,
- timeline_event_tag_names: ['start time', 'end time']
+ timeline_event_tag_names: ['start time', 'end time', 'response initiated']
}
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')
+ expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly(
+ 'Start time', 'End time', 'Response initiated')
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')
+ expect(project.incident_management_timeline_event_tags.pluck_names).to include(
+ 'Start time', 'End time', 'Response initiated')
end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index dc72cf04776..7ba349ceeae 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -117,11 +117,37 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
+ shared_examples 'bulk update service' do
+ it 'result count only includes authorized issuables' do
+ all_issues = issues + [create(:issue, project: create(:project, :private))]
+ result = bulk_update(all_issues, { assignee_ids: [user.id] })
+
+ expect(result[:count]).to eq(issues.count)
+ end
+
+ context 'when issuable_ids are passed as an array' do
+ it 'updates assignees' do
+ expect do
+ described_class.new(
+ parent,
+ user,
+ { issuable_ids: issues.map(&:id), assignee_ids: [user.id] }
+ ).execute('issue')
+
+ issues.each(&:reset)
+ end.to change { issues.flat_map(&:assignee_ids) }.from([]).to([user.id] * 2)
+ end
+ end
+ end
+
context 'with issuables at a project level' do
+ let_it_be_with_reload(:issues) { create_list(:issue, 2, project: project) }
+
let(:parent) { project }
+ it_behaves_like 'bulk update service'
+
context 'with unpermitted attributes' do
- let(:issues) { create_list(:issue, 2, project: project) }
let(:label) { create(:label, project: project) }
it 'does not update the issues' do
@@ -131,9 +157,23 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- describe 'close issues' do
- let(:issues) { create_list(:issue, 2, project: project) }
+ context 'when issuable update service raises an ArgumentError' do
+ before do
+ allow_next_instance_of(Issues::UpdateService) do |update_service|
+ allow(update_service).to receive(:execute).and_raise(ArgumentError, 'update error')
+ end
+ end
+
+ it 'returns an error response' do
+ result = bulk_update(issues, add_label_ids: [])
+ expect(result).to be_error
+ expect(result.errors).to contain_exactly('update error')
+ expect(result.http_status).to eq(422)
+ end
+ end
+
+ describe 'close issues' do
it 'succeeds and returns the correct number of issues updated' do
result = bulk_update(issues, state_event: 'close')
@@ -155,24 +195,24 @@ RSpec.describe Issuable::BulkUpdateService do
end
describe 'reopen issues' do
- let(:issues) { create_list(:closed_issue, 2, project: project) }
+ let_it_be_with_reload(:closed_issues) { create_list(:closed_issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
- result = bulk_update(issues, state_event: 'reopen')
+ result = bulk_update(closed_issues, state_event: 'reopen')
expect(result.success?).to be_truthy
- expect(result.payload[:count]).to eq(issues.count)
+ expect(result.payload[:count]).to eq(closed_issues.count)
end
it 'reopens all the issues passed' do
- bulk_update(issues, state_event: 'reopen')
+ bulk_update(closed_issues, state_event: 'reopen')
expect(project.issues.closed).to be_empty
expect(project.issues.opened).not_to be_empty
end
it_behaves_like 'scheduling cached group count clear' do
- let(:issuables) { issues }
+ let(:issuables) { closed_issues }
let(:params) { { state_event: 'reopen' } }
end
end
@@ -207,10 +247,10 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- context 'when the new assignee ID is not present' do
- it 'does not unassign' do
+ context 'when the new assignee IDs array is empty' do
+ it 'removes all assignees' do
expect { bulk_update(merge_request, assignee_ids: []) }
- .not_to change { merge_request.reload.assignee_ids }
+ .to change(merge_request.assignees, :count).by(-1)
end
end
end
@@ -244,10 +284,10 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
- context 'when the new assignee ID is not present' do
- it 'does not unassign' do
+ context 'when the new assignee IDs array is empty' do
+ it 'removes all assignees' do
expect { bulk_update(issue, assignee_ids: []) }
- .not_to change(issue.assignees, :count)
+ .to change(issue.assignees, :count).by(-1)
end
end
end
@@ -321,6 +361,10 @@ RSpec.describe Issuable::BulkUpdateService do
group.add_reporter(user)
end
+ it_behaves_like 'bulk update service' do
+ let_it_be_with_reload(:issues) { create_list(:issue, 2, project: create(:project, group: group)) }
+ end
+
describe 'updating milestones' do
let(:milestone) { create(:milestone, group: group) }
let(:project) { create(:project, :repository, group: group) }
@@ -372,4 +416,13 @@ RSpec.describe Issuable::BulkUpdateService do
end
end
end
+
+ context 'when no parent is provided' do
+ it 'returns an unscoped update error' do
+ result = described_class.new(nil, user, { assignee_ids: [user.id], issuable_ids: [] }).execute('issue')
+
+ expect(result).to be_error
+ expect(result.errors).to contain_exactly(_('A parent must be provided when bulk updating issuables'))
+ end
+ end
end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index c72d48d5b77..29f548e1c47 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Issuable::DestroyService do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
- subject(:service) { described_class.new(project: project, current_user: user) }
+ subject(:service) { described_class.new(container: project, current_user: user) }
describe '#execute' do
context 'when issuable is an issue' do
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
index ecdd8d031c9..a6f57088ad1 100644
--- a/spec/services/issuable/discussions_list_service_spec.rb
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Issuable::DiscussionsListService do
let_it_be(:project) { create(:project, :repository, :private, group: group) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:label_2) { create(:label, project: project) }
let(:finder_params_for_issuable) { {} }
@@ -22,8 +23,7 @@ RSpec.describe Issuable::DiscussionsListService 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] })
+ WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true)
end
it "returns no notes" do
diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb
index 6b720d6e687..39a6799dbad 100644
--- a/spec/services/issues/after_create_service_spec.rb
+++ b/spec/services/issues/after_create_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Issues::AfterCreateService do
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:issue) { create(:issue, project: project, author: current_user, milestone: milestone, assignee_ids: [assignee.id]) }
- subject(:after_create_service) { described_class.new(project: project, current_user: current_user) }
+ subject(:after_create_service) { described_class.new(container: project, current_user: current_user) }
describe '#execute' do
it 'creates a pending todo for new assignee' do
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 838e0675372..2160c45d079 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Issues::BuildService do
end
def build_issue(issue_params = {})
- described_class.new(project: project, current_user: user, params: issue_params).execute
+ described_class.new(container: project, current_user: user, params: issue_params).execute
end
context 'for a single discussion' do
@@ -45,7 +45,7 @@ RSpec.describe Issues::BuildService do
describe '#items_for_discussions' do
it 'has an item for each discussion' do
create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13)
- service = described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid })
+ service = described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid })
service.execute
@@ -54,7 +54,7 @@ RSpec.describe Issues::BuildService do
end
describe '#item_for_discussion' do
- let(:service) { described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
+ let(:service) { described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
it 'mentions the author of the note' do
discussion = create(:diff_note_on_merge_request, author: create(:user, username: 'author')).to_discussion
diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb
index 67f32b85350..eafaea93015 100644
--- a/spec/services/issues/clone_service_spec.rb
+++ b/spec/services/issues/clone_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Issues::CloneService do
let(:with_notes) { false }
subject(:clone_service) do
- described_class.new(project: old_project, current_user: user)
+ described_class.new(container: old_project, current_user: user)
end
shared_context 'user can clone issue' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index ef24d1e940e..803808e667c 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -20,13 +20,13 @@ RSpec.describe Issues::CloseService do
end
describe '#execute' do
- let(:service) { described_class.new(project: project, current_user: user) }
+ let(:service) { described_class.new(container: project, current_user: user) }
context 'when skip_authorization is true' do
it 'does close the issue even if user is not authorized' do
non_authorized_user = create(:user)
- service = described_class.new(project: project, current_user: non_authorized_user)
+ service = described_class.new(container: project, current_user: non_authorized_user)
expect do
service.execute(issue, skip_authorization: true)
@@ -167,7 +167,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
@@ -178,7 +178,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
@@ -189,7 +189,7 @@ RSpec.describe Issues::CloseService do
project.reload
expect(project.external_issue_tracker).not_to receive(:close_issue)
- described_class.new(project: project, current_user: user).close_issue(external_issue)
+ described_class.new(container: project, current_user: user).close_issue(external_issue)
end
end
end
@@ -197,7 +197,7 @@ RSpec.describe Issues::CloseService do
context "closed by a merge request", :sidekiq_might_not_need_inline do
subject(:close_issue) do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_merge_request)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_merge_request)
end
end
@@ -266,7 +266,7 @@ RSpec.describe Issues::CloseService do
context "closed by a commit", :sidekiq_might_not_need_inline do
it 'mentions closure via a commit' do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit)
end
email = ActionMailer::Base.deliveries.last
@@ -280,7 +280,7 @@ RSpec.describe Issues::CloseService do
it 'does not mention the commit id' do
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
+ described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit)
end
email = ActionMailer::Base.deliveries.last
@@ -296,7 +296,7 @@ RSpec.describe Issues::CloseService do
context "valid params" do
subject(:close_issue) do
perform_enqueued_jobs do
- described_class.new(project: project, current_user: user).close_issue(issue)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
@@ -438,7 +438,7 @@ RSpec.describe Issues::CloseService do
expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
- described_class.new(project: project, current_user: user).close_issue(issue)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
@@ -449,7 +449,7 @@ RSpec.describe Issues::CloseService do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :confidential_issue_hooks)
- described_class.new(project: project, current_user: user).close_issue(issue)
+ described_class.new(container: project, current_user: user).close_issue(issue)
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 7ab2046b6be..ada5b300d7a 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Issues::CreateService do
let(:opts) { { title: 'title' } }
let(:spam_params) { double }
- let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) }
+ let(:service) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params) }
it_behaves_like 'rate limited service' do
let(:key) { :issues_create }
@@ -147,7 +147,7 @@ RSpec.describe Issues::CreateService do
end
context 'when a build_service is provided' do
- let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
+ let(:result) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute }
let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') }
let(:build_service) { double(:build_service, execute: issue_from_builder) }
@@ -160,7 +160,7 @@ RSpec.describe Issues::CreateService do
end
context 'when skip_system_notes is true' do
- let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
+ let(:issue) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) }
it 'does not call Issuable::CommonSystemNotesService' do
expect(Issuable::CommonSystemNotesService).not_to receive(:new)
@@ -256,7 +256,7 @@ RSpec.describe Issues::CreateService do
let_it_be(:non_member) { create(:user) }
it 'filters out params that cannot be set without the :set_issue_metadata permission' do
- result = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: non_member, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -270,7 +270,7 @@ RSpec.describe Issues::CreateService do
end
it 'can create confidential issues' do
- result = described_class.new(project: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -281,7 +281,7 @@ RSpec.describe Issues::CreateService do
it 'moves the issue to the end, in an asynchronous worker' do
expect(Issues::PlacementWorker).to receive(:perform_async).with(be_nil, Integer)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'when label belongs to project group' do
@@ -368,7 +368,7 @@ RSpec.describe Issues::CreateService do
it 'invalidates open issues counter for assignees when issue is assigned' do
project.add_maintainer(assignee)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
expect(assignee.assigned_open_issues_count).to eq 1
end
@@ -439,7 +439,7 @@ RSpec.describe Issues::CreateService do
expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
context 'when issue is confidential' do
@@ -462,7 +462,7 @@ RSpec.describe Issues::CreateService do
expect(project).to receive(:execute_hooks).with(expected_payload, :confidential_issue_hooks)
expect(project).to receive(:execute_integrations).with(expected_payload, :confidential_issue_hooks)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
end
end
end
@@ -508,7 +508,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is invalid' do
opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -518,7 +518,7 @@ RSpec.describe Issues::CreateService do
it 'removes assignee when user id is 0' do
opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -529,7 +529,7 @@ RSpec.describe Issues::CreateService do
project.add_maintainer(assignee)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -549,7 +549,7 @@ RSpec.describe Issues::CreateService do
project.update!(visibility_level: level)
opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
- result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
issue = result[:issue]
expect(result).to be_success
@@ -561,10 +561,40 @@ RSpec.describe Issues::CreateService do
end
it_behaves_like 'issuable record that supports quick actions' do
- let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] }
+ let(:issuable) { described_class.new(container: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] }
end
context 'Quick actions' do
+ context 'as work item' do
+ let(:opts) do
+ {
+ title: "My work item",
+ work_item_type: work_item_type,
+ description: "/shrug"
+ }
+ end
+
+ context 'when work item type is not the default Issue' do
+ let(:work_item_type) { create(:work_item_type, namespace: project.namespace) }
+
+ it 'saves the work item without applying the quick action' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue.description).to eq("/shrug")
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:work_item_type) { WorkItems::Type.default_by_type(:issue) }
+
+ it 'saves the work item and applies the quick action' do
+ expect(result).to be_success
+ expect(issue).to be_persisted
+ expect(issue.description).to eq(" ¯\\_(ツ)_/¯")
+ end
+ end
+ end
+
context 'with assignee, milestone, and contact in params and command' do
let_it_be(:contact) { create(:contact, group: group) }
@@ -672,14 +702,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@@ -688,7 +718,7 @@ RSpec.describe Issues::CreateService do
it 'sets default title and description values if not provided' do
result = described_class.new(
- project: project, current_user: user,
+ container: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
@@ -702,7 +732,7 @@ RSpec.describe Issues::CreateService do
it 'takes params from the request over the default values' do
result = described_class.new(
- project: project,
+ container: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
@@ -723,14 +753,14 @@ RSpec.describe Issues::CreateService do
let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
it 'resolves the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
discussion.first_note.reload
expect(discussion.resolved?).to be(true)
end
it 'added a system note to the discussion' do
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute
reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first
@@ -739,7 +769,7 @@ RSpec.describe Issues::CreateService do
it 'sets default title and description values if not provided' do
result = described_class.new(
- project: project, current_user: user,
+ container: project, current_user: user,
params: opts,
spam_params: spam_params
).execute
@@ -753,7 +783,7 @@ RSpec.describe Issues::CreateService do
it 'takes params from the request over the default values' do
result = described_class.new(
- project: project,
+ container: project,
current_user: user,
params: opts.merge(
description: 'Custom issue description',
@@ -806,7 +836,7 @@ RSpec.describe Issues::CreateService do
end
subject do
- described_class.new(project: project, current_user: user, params: params, spam_params: spam_params)
+ described_class.new(container: project, current_user: user, params: params, spam_params: spam_params)
end
it 'executes SpamActionService' do
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
index 0eb0bbb1480..f49bce70cd0 100644
--- a/spec/services/issues/duplicate_service_spec.rb
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Issues::DuplicateService do
let(:canonical_issue) { create(:issue, project: canonical_project) }
let(:duplicate_issue) { create(:issue, project: duplicate_project) }
- subject { described_class.new(project: duplicate_project, current_user: user) }
+ subject { described_class.new(container: duplicate_project, current_user: user) }
describe '#execute' do
context 'when the issues passed are the same' do
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index d3359447fd8..1ac64c0301d 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ExportCsvService, :with_license do
+RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
@@ -57,137 +57,151 @@ RSpec.describe Issues::ExportCsvService, :with_license do
time_estimate: 72000)
end
- it 'includes the columns required for import' do
- expect(csv.headers).to include('Title', 'Description')
- end
-
- it 'returns two issues' do
- expect(csv.count).to eq(2)
- end
+ shared_examples 'exports CSVs for issues' do
+ it 'includes the columns required for import' do
+ expect(csv.headers).to include('Title', 'Description')
+ end
- specify 'iid' do
- expect(csv[0]['Issue ID']).to eq issue.iid.to_s
- end
+ it 'returns two issues' do
+ expect(csv.count).to eq(2)
+ end
- specify 'url' do
- expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
- end
+ specify 'iid' do
+ expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ end
- specify 'title' do
- expect(csv[0]['Title']).to eq issue.title
- end
+ specify 'url' do
+ expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
+ end
- specify 'state' do
- expect(csv[0]['State']).to eq 'Open'
- end
+ specify 'title' do
+ expect(csv[0]['Title']).to eq issue.title
+ end
- specify 'description' do
- expect(csv[0]['Description']).to eq issue.description
- expect(csv[1]['Description']).to eq nil
- end
+ specify 'state' do
+ expect(csv[0]['State']).to eq 'Open'
+ end
- specify 'author name' do
- expect(csv[0]['Author']).to eq issue.author_name
- end
+ specify 'description' do
+ expect(csv[0]['Description']).to eq issue.description
+ expect(csv[1]['Description']).to eq nil
+ end
- specify 'author username' do
- expect(csv[0]['Author Username']).to eq issue.author.username
- end
+ specify 'author name' do
+ expect(csv[0]['Author']).to eq issue.author_name
+ end
- specify 'assignee name' do
- expect(csv[0]['Assignee']).to eq user.name
- expect(csv[1]['Assignee']).to eq ''
- end
+ specify 'author username' do
+ expect(csv[0]['Author Username']).to eq issue.author.username
+ end
- specify 'assignee username' do
- expect(csv[0]['Assignee Username']).to eq user.username
- expect(csv[1]['Assignee Username']).to eq ''
- end
+ specify 'assignee name' do
+ expect(csv[0]['Assignee']).to eq user.name
+ expect(csv[1]['Assignee']).to eq ''
+ end
- specify 'confidential' do
- expect(csv[0]['Confidential']).to eq 'No'
- end
+ specify 'assignee username' do
+ expect(csv[0]['Assignee Username']).to eq user.username
+ expect(csv[1]['Assignee Username']).to eq ''
+ end
- specify 'milestone' do
- expect(csv[0]['Milestone']).to eq issue.milestone.title
- expect(csv[1]['Milestone']).to eq nil
- end
+ specify 'confidential' do
+ expect(csv[0]['Confidential']).to eq 'No'
+ end
- specify 'labels' do
- expect(csv[0]['Labels']).to eq 'Feature,Idea'
- expect(csv[1]['Labels']).to eq nil
- end
+ specify 'milestone' do
+ expect(csv[0]['Milestone']).to eq issue.milestone.title
+ expect(csv[1]['Milestone']).to eq nil
+ end
- specify 'due_date' do
- expect(csv[0]['Due Date']).to eq '2014-03-02'
- expect(csv[1]['Due Date']).to eq nil
- end
+ specify 'labels' do
+ expect(csv[0]['Labels']).to eq 'Feature,Idea'
+ expect(csv[1]['Labels']).to eq nil
+ end
- specify 'created_at' do
- expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
- end
+ specify 'due_date' do
+ expect(csv[0]['Due Date']).to eq '2014-03-02'
+ expect(csv[1]['Due Date']).to eq nil
+ end
- specify 'updated_at' do
- expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
- end
+ specify 'created_at' do
+ expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
+ end
- specify 'closed_at' do
- expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
- expect(csv[1]['Closed At (UTC)']).to eq nil
- end
+ specify 'updated_at' do
+ expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
+ end
- specify 'discussion_locked' do
- expect(csv[0]['Locked']).to eq 'Yes'
- end
+ specify 'closed_at' do
+ expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
+ expect(csv[1]['Closed At (UTC)']).to eq nil
+ end
- specify 'weight' do
- expect(csv[0]['Weight']).to eq '4'
- end
+ specify 'discussion_locked' do
+ expect(csv[0]['Locked']).to eq 'Yes'
+ end
- specify 'time estimate' do
- expect(csv[0]['Time Estimate']).to eq '72000'
- expect(csv[1]['Time Estimate']).to eq '0'
- end
+ specify 'weight' do
+ expect(csv[0]['Weight']).to eq '4'
+ end
- specify 'time spent' do
- expect(csv[0]['Time Spent']).to eq '560'
- expect(csv[1]['Time Spent']).to eq '0'
- end
+ specify 'time estimate' do
+ expect(csv[0]['Time Estimate']).to eq '72000'
+ expect(csv[1]['Time Estimate']).to eq '0'
+ end
- context 'with issues filtered by labels and project' do
- subject do
- described_class.new(
- IssuesFinder.new(user,
- project_id: project.id,
- label_name: %w(Idea Feature)).execute, project)
+ specify 'time spent' do
+ expect(csv[0]['Time Spent']).to eq '560'
+ expect(csv[1]['Time Spent']).to eq '0'
end
- it 'returns only filtered objects' do
- expect(csv.count).to eq(1)
- expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ context 'with issues filtered by labels and project' do
+ subject do
+ described_class.new(
+ IssuesFinder.new(user,
+ project_id: project.id,
+ label_name: %w(Idea Feature)).execute, project)
+ end
+
+ it 'returns only filtered objects' do
+ expect(csv.count).to eq(1)
+ expect(csv[0]['Issue ID']).to eq issue.iid.to_s
+ end
end
- end
- context 'with label links' do
- let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }
+ context 'with label links' do
+ let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }
- it 'does not run a query for each label link' do
- control_count = ActiveRecord::QueryRecorder.new { csv }.count
+ it 'does not run a query for each label link' do
+ control_count = ActiveRecord::QueryRecorder.new { csv }.count
- labeled_issues
+ labeled_issues
- expect { csv }.not_to exceed_query_limit(control_count)
- expect(csv.count).to eq(4)
- end
+ expect { csv }.not_to exceed_query_limit(control_count)
+ expect(csv.count).to eq(4)
+ end
- it 'returns the labels in sorted order' do
- labeled_issues
+ it 'returns the labels in sorted order' do
+ labeled_issues
- labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
- expect(labeled_rows.count).to eq(2)
- expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea"))
+ labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
+ expect(labeled_rows.count).to eq(2)
+ expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea"))
+ end
end
end
+
+ context 'with export_csv_preload_in_batches feature flag disabled' do
+ before do
+ stub_feature_flags(export_csv_preload_in_batches: false)
+ end
+
+ it_behaves_like 'exports CSVs for issues'
+ end
+
+ context 'with export_csv_preload_in_batches feature flag enabled' do
+ it_behaves_like 'exports CSVs for issues'
+ end
end
context 'with minimal details' do
diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb
index 9ad1d7dba9f..90e360f9cf1 100644
--- a/spec/services/issues/import_csv_service_spec.rb
+++ b/spec/services/issues/import_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::ImportCsvService do
+RSpec.describe Issues::ImportCsvService, feature_category: :team_planning do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:assignee) { create(:user, username: 'csv_assignee') }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 324b2aa9fe2..12924df3200 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issues::MoveService do
+RSpec.describe Issues::MoveService, feature_category: :team_planning do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user) }
@@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do
end
subject(:move_service) do
- described_class.new(project: old_project, current_user: user)
+ described_class.new(container: old_project, current_user: user)
end
shared_context 'user can move issue' do
diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb
index 16166c1fa33..aee3583b834 100644
--- a/spec/services/issues/referenced_merge_requests_service_spec.rb
+++ b/spec/services/issues/referenced_merge_requests_service_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService do
let_it_be(:referencing_mr) { create_referencing_mr(source_project: project, source_branch: 'csv') }
let_it_be(:referencing_mr_other_project) { create_referencing_mr(source_project: other_project, source_branch: 'csv') }
- let(:service) { described_class.new(project: project, current_user: user) }
+ let(:service) { described_class.new(container: project, current_user: user) }
describe '#execute' do
it 'returns a list of sorted merge requests' do
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index 95d456c1b05..05c61d0abfc 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Issues::RelatedBranchesService do
let(:user) { developer }
- subject { described_class.new(project: project, current_user: user) }
+ subject { described_class.new(container: project, current_user: user) }
before_all do
project.add_developer(developer)
@@ -54,7 +54,7 @@ RSpec.describe Issues::RelatedBranchesService do
merge_request.create_cross_references!(user)
referenced_merge_requests = Issues::ReferencedMergeRequestsService
- .new(project: issue.project, current_user: user)
+ .new(container: issue.project, current_user: user)
.referenced_merge_requests(issue)
expect(referenced_merge_requests).not_to be_empty
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 529b3ff266b..68015a2327e 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Issues::ReopenService do
guest = create(:user)
project.add_guest(guest)
- described_class.new(project: project, current_user: guest).execute(issue)
+ described_class.new(container: project, current_user: guest).execute(issue)
expect(issue).to be_closed
end
@@ -21,7 +21,7 @@ RSpec.describe Issues::ReopenService do
it 'does close the issue even if user is not authorized' do
non_authorized_user = create(:user)
- service = described_class.new(project: project, current_user: non_authorized_user)
+ service = described_class.new(container: project, current_user: non_authorized_user)
expect do
service.execute(issue, skip_authorization: true)
@@ -33,7 +33,7 @@ RSpec.describe Issues::ReopenService do
context 'when user is authorized to reopen issue' do
let(:user) { create(:user) }
- subject(:execute) { described_class.new(project: project, current_user: user).execute(issue) }
+ subject(:execute) { described_class.new(container: project, current_user: user).execute(issue) }
before do
project.add_maintainer(user)
diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb
index 392930c1b9f..430a9e9f526 100644
--- a/spec/services/issues/reorder_service_spec.rb
+++ b/spec/services/issues/reorder_service_spec.rb
@@ -85,6 +85,6 @@ RSpec.describe Issues::ReorderService do
end
def service(params)
- described_class.new(project: project, current_user: user, params: params)
+ described_class.new(container: project, current_user: user, params: params)
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 930766c520b..973025bd2e3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
def update_issue(opts)
- described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
it_behaves_like 'issuable update service updating last_edited_at values' do
@@ -106,29 +106,29 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'when updating milestone' do
before do
- update_issue({ milestone: nil })
+ update_issue({ milestone_id: nil })
end
it 'updates issue milestone when passing `milestone` param' do
- expect { update_issue({ milestone: milestone }) }
+ expect { update_issue({ milestone_id: milestone.id }) }
.to change(issue, :milestone).to(milestone).from(nil)
end
it "triggers 'issuableMilestoneUpdated'" do
expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original
- update_issue({ milestone: milestone })
+ update_issue({ milestone_id: milestone.id })
end
context 'when milestone remains unchanged' do
before do
- update_issue({ title: 'abc', milestone: milestone })
+ update_issue({ title: 'abc', milestone_id: milestone.id })
end
it "does not trigger 'issuableMilestoneUpdated'" do
expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
- update_issue({ milestone: milestone })
+ update_issue({ milestone_id: milestone.id })
end
end
end
@@ -420,7 +420,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue_1.id, issue_2.id]
- described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3)
+ described_class.new(container: issue_3.project, current_user: user, params: opts).execute(issue_3)
expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position)
end
end
@@ -428,7 +428,7 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'when current user cannot admin issues in the project' do
it 'filters out params that cannot be set without the :admin_issue permission' do
described_class.new(
- project: project, current_user: guest, params: opts.merge(
+ container: project, current_user: guest, params: opts.merge(
confidential: true,
issue_type: 'test_case'
)
@@ -755,14 +755,14 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'marks todos as done' do
- update_issue(milestone: create(:milestone, project: project))
+ update_issue(milestone_id: create(:milestone, project: project).id)
expect(todo.reload.done?).to eq true
end
it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
- update_issue(milestone: create(:milestone, project: project))
+ update_issue(milestone_id: create(:milestone, project: project).id)
end
should_email(subscriber)
@@ -779,7 +779,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(service).to receive(:delete_cache).and_call_original
end
- update_issue(milestone: milestone)
+ update_issue(milestone_id: milestone.id)
end
end
@@ -803,7 +803,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(service).to receive(:delete_cache).and_call_original
end
- update_issue(milestone: new_milestone)
+ update_issue(milestone_id: new_milestone.id)
end
end
@@ -838,7 +838,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_email(subscriber)
@@ -854,7 +854,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_not_email(subscriber)
@@ -865,7 +865,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
- @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue)
+ @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue)
end
should_not_email(subscriber)
@@ -897,7 +897,7 @@ RSpec.describe Issues::UpdateService, :mailer do
line_number: 1
}
}
- service = described_class.new(project: project, current_user: user, params: params)
+ service = described_class.new(container: project, current_user: user, params: params)
expect(Spam::SpamActionService).not_to receive(:new)
@@ -915,7 +915,7 @@ RSpec.describe Issues::UpdateService, :mailer do
line_number: 1
}
}
- service = described_class.new(project: project, current_user: user, params: params)
+ service = described_class.new(container: project, current_user: user, params: params)
expect(service).to receive(:after_update).with(issue, {})
@@ -991,7 +991,7 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating labels' do
let(:label3) { create(:label, project: project) }
- let(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue).reload }
+ let(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue).reload }
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
@@ -1063,7 +1063,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
context 'updating dates' do
- subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) }
+ subject(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue) }
let(:updated_date) { 1.week.from_now.to_date }
@@ -1428,7 +1428,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'raises an error for invalid move ids' do
opts = { move_between_ids: [9000, non_existing_record_id] }
- expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
+ expect { described_class.new(container: issue.project, current_user: user, params: opts).execute(issue) }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
@@ -1473,7 +1473,33 @@ RSpec.describe Issues::UpdateService, :mailer do
it_behaves_like 'issuable record that supports quick actions' do
let(:existing_issue) { create(:issue, project: project) }
- let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute(existing_issue) }
+ let(:issuable) { described_class.new(container: project, current_user: user, params: params).execute(existing_issue) }
+ end
+
+ context 'with quick actions' do
+ context 'as work item' do
+ let(:opts) { { description: "/shrug" } }
+
+ context 'when work item type is not the default Issue' do
+ let(:issue) { create(:work_item, :task, description: "") }
+
+ it 'does not apply the quick action' do
+ expect do
+ update_issue(opts)
+ end.to change(issue, :description).to("/shrug")
+ end
+ end
+
+ context 'when work item type is the default Issue' do
+ let(:issue) { create(:work_item, :issue, description: "") }
+
+ it 'does not apply the quick action' do
+ expect do
+ update_issue(opts)
+ end.to change(issue, :description).to(" ¯\\_(ツ)_/¯")
+ end
+ end
+ end
end
end
end
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index ad1f91ab5e6..230e4c1b5e1 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Issues::ZoomLinkService do
let_it_be(:issue) { create(:issue) }
let(:project) { issue.project }
- let(:service) { described_class.new(project: project, current_user: user, params: { issue: issue }) }
+ let(:service) { described_class.new(container: project, current_user: user, params: { issue: issue }) }
let(:zoom_link) { 'https://zoom.us/j/123456789' }
before do
diff --git a/spec/services/jira_connect_installations/update_service_spec.rb b/spec/services/jira_connect_installations/update_service_spec.rb
index ec5bb5d6d6a..15f3b485b20 100644
--- a/spec/services/jira_connect_installations/update_service_spec.rb
+++ b/spec/services/jira_connect_installations/update_service_spec.rb
@@ -45,8 +45,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
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_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
@@ -62,19 +63,19 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
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')
-
+ it 'sends an installed event to the instance and updates instance_url' do
expect(JiraConnectInstallations::ProxyLifecycleEventService)
.to receive(:execute).with(installation, :installed, 'https://gitlab.example.com')
.and_return(ServiceResponse.new(status: :success))
+ expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)
+
execute_service
expect(installation.instance_url).to eq(update_params[:instance_url])
end
- context 'and the new instance_url is empty' do
+ context 'and the new instance_url is nil' do
let(:update_params) { { instance_url: nil } }
it 'starts an async worker to send an uninstalled event to the previous instance' do
@@ -98,8 +99,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ
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_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
diff --git a/spec/services/keys/revoke_service_spec.rb b/spec/services/keys/revoke_service_spec.rb
new file mode 100644
index 00000000000..ec07701b4b7
--- /dev/null
+++ b/spec/services/keys/revoke_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Keys::RevokeService, feature_category: :source_code_management do
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user) }
+
+ it 'destroys a key' do
+ key = create(:key)
+
+ expect { service.execute(key) }.to change { key.persisted? }.from(true).to(false)
+ end
+
+ it 'unverifies associated signatures' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect do
+ service.execute(key)
+ end.to change { signature.reload.key }.from(key).to(nil)
+ .and change { signature.reload.verification_status }.from('verified').to('revoked_key')
+ end
+
+ it 'does not unverifies signatures if destroy fails' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect(key).to receive(:destroy).and_return(false)
+
+ expect { service.execute(key) }.not_to change { signature.reload.verification_status }
+ expect(key).to be_persisted
+ end
+
+ context 'when revoke_ssh_signatures disabled' do
+ before do
+ stub_feature_flags(revoke_ssh_signatures: false)
+ end
+
+ it 'does not unverifies signatures' do
+ key = create(:key)
+ signature = create(:ssh_signature, key: key)
+
+ expect { service.execute(key) }.not_to change { signature.reload.verification_status }
+ end
+ end
+end
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index 9d4d8851c2d..c90d7af022f 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Lfs::FileTransformer, feature_category: :git_lfs do
+RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management do
let(:project) { create(:project, :repository, :wiki_repo) }
let(:repository) { project.repository }
let(:file_content) { 'Test file content' }
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index d26bab7bb0a..ca5c052d032 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -30,6 +30,14 @@ RSpec.describe Members::ApproveAccessRequestService do
expect(member.requested_at).to be_nil
end
+ it 'calls the method to resolve access request for the approver' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ end
+
+ described_class.new(current_user, params).execute(access_requester, **opts)
+ end
+
context 'with a custom access level' do
let(:params) { { access_level: custom_access_level } }
diff --git a/spec/services/members/base_service_spec.rb b/spec/services/members/base_service_spec.rb
new file mode 100644
index 00000000000..b2db599db9c
--- /dev/null
+++ b/spec/services/members/base_service_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Members::BaseService, feature_category: :projects do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:access_requester) { create(:group_member) }
+
+ describe '#resolve_access_request_todos' do
+ it 'calls the resolve_access_request_todos of todo service' do
+ expect_next_instance_of(TodoService) do |todo_service|
+ expect(todo_service)
+ .to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ end
+
+ described_class.new.send(:resolve_access_request_todos, current_user, access_requester)
+ end
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index d8a8d5881bf..2b956bec469 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -41,6 +41,14 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
.not_to change { member_user.notification_settings.count }
end
end
+
+ it 'resolves the access request todos for the owner' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:resolve_access_request_todos).with(current_user, member)
+ end
+
+ described_class.new(current_user).execute(member, **opts)
+ end
end
shared_examples 'a service destroying a member with access' do
@@ -111,26 +119,6 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
subject(:destroy_member) { service_object.execute(member_to_delete, **opts) }
- shared_examples_for 'deletes the member without using a lock' do
- it 'does not try to perform the delete within a lock' do
- # `UpdateHighestRole` concern also uses locks to peform work
- # whenever a Member is committed, so that needs to be accounted for.
- lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
- expect(Gitlab::ExclusiveLease)
- .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
-
- # We do not use any locks for member deletion process.
- expect(Gitlab::ExclusiveLease)
- .not_to receive(:new).with(lock_key, timeout: timeout)
-
- destroy_member
- end
-
- it 'destroys the membership' do
- expect { destroy_member }.to change { entity.members.count }.by(-1)
- end
- end
-
context 'for group members' do
before do
group.add_owner(current_user)
@@ -171,13 +159,70 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
context 'deleting group members that are not owners' do
let!(:member_to_delete) { group.add_developer(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
- let(:entity) { group }
+ it 'does not try to perform the deletion of the member within a lock' do
+ # We need to account for other places involved in the Member deletion process that
+ # uses ExclusiveLease.
+
+ # 1. `UpdateHighestRole` concern uses locks to peform work
+ # whenever a Member is committed, so that needs to be accounted for.
+ lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
+
+ # 2. `Users::RefreshAuthorizedProjectsService` also uses locks to perform work,
+ # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well.
+ lock_key_for_authorizations_refresh = "refresh_authorized_projects:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 1.minute.to_i).and_call_original
+
+ # We do not use any locks for the member deletion process, from within this service.
+ expect(Gitlab::ExclusiveLease)
+ .not_to receive(:new).with(lock_key, timeout: timeout)
+
+ destroy_member
+ end
+
+ it 'destroys the membership' do
+ expect { destroy_member }.to change { group.members.count }.by(-1)
end
end
end
context 'for project members' do
+ shared_examples_for 'deletes the project member without using a lock' do
+ it 'does not try to perform the deletion of a project member within a lock' do
+ # We need to account for other places involved in the Member deletion process that
+ # uses ExclusiveLease.
+
+ # 1. `UpdateHighestRole` concern uses locks to peform work
+ # whenever a Member is committed, so that needs to be accounted for.
+ lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original
+
+ # 2. `AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker` also uses locks to perform work,
+ # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well.
+ lock_key_for_authorizations_refresh =
+ "authorized_project_update/project_recalculate_worker/projects/#{member_to_delete.project.id}"
+
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 10.seconds).and_call_original
+
+ # We do not use any locks for the member deletion process, from within this service.
+ expect(Gitlab::ExclusiveLease)
+ .not_to receive(:new).with(lock_key, timeout: timeout)
+
+ destroy_member
+ end
+
+ it 'destroys the membership' do
+ expect { destroy_member }.to change { entity.members.count }.by(-1)
+ end
+ end
+
before do
group_project.add_owner(current_user)
end
@@ -186,16 +231,16 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
context 'deleting project owners' do
let!(:member_to_delete) { entity.add_owner(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
+ it_behaves_like 'deletes the project member without using a lock' do
let(:entity) { group_project }
end
end
end
- context 'deleting project memebrs that are not owners' do
+ context 'deleting project members that are not owners' do
let!(:member_to_delete) { group_project.add_developer(member_user) }
- it_behaves_like 'deletes the member without using a lock' do
+ it_behaves_like 'deletes the project member without using a lock' do
let(:entity) { group_project }
end
end
diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb
index 8304bee2ffc..5dfba7adf0f 100644
--- a/spec/services/members/projects/creator_service_spec.rb
+++ b/spec/services/members/projects/creator_service_spec.rb
@@ -27,6 +27,8 @@ RSpec.describe Members::Projects::CreatorService do
context 'authorized projects update' do
it 'schedules a single project authorization update job when called multiple times' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once
1.upto(3) do
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index f477b2166d9..f2823b1f0c7 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::AfterCreateService do
+RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request) }
subject(:after_create_service) do
@@ -126,6 +126,17 @@ RSpec.describe MergeRequests::AfterCreateService do
end
end
+ it 'updates the prepared_at' do
+ # Need to reset the `prepared_at` since it can be already set in preceding tests.
+ merge_request.update!(prepared_at: nil)
+
+ freeze_time do
+ expect { execute_service }.to change { merge_request.prepared_at }
+ .from(nil)
+ .to(Time.current)
+ end
+ end
+
it 'increments the usage data counter of create event' do
counter = Gitlab::UsageDataCounters::MergeRequestCounter
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 79c779678a4..0fcfc16af73 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe MergeRequests::BuildService do
+RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workflow do
using RSpec::Parameterized::TableSyntax
include RepoHelpers
include ProjectForksHelper
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b3c4ed4c544..2c0817550c6 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CloseService do
+RSpec.describe MergeRequests::CloseService, feature_category: :code_review_workflow do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 0eefbed252b..7bb0dd723a1 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateFromIssueService do
+RSpec.describe MergeRequests::CreateFromIssueService, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index 7984fff3031..f11e3d0d1df 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache do
include ProjectForksHelper
- let_it_be(:project, reload: true) { create(:project, :repository) }
+ let_it_be(:project, refind: true) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(project: project, current_user: actor, params: params) }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index da8e8d944d6..394fc269ac3 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
+RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, feature_category: :code_review_workflow do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
@@ -501,40 +501,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
project.add_developer(user)
end
- context 'when async_merge_request_diff_creation is enabled' do
- before do
- stub_feature_flags(async_merge_request_diff_creation: true)
- end
-
- it 'creates the merge request', :sidekiq_inline do
- expect_next_instance_of(MergeRequest) do |instance|
- expect(instance).not_to receive(:eager_fetch_ref!)
- end
-
- merge_request = described_class.new(project: project, current_user: user, params: opts).execute
-
- expect(merge_request).to be_persisted
- expect(merge_request.iid).to be > 0
- expect(merge_request.merge_request_diff).not_to be_empty
- end
- end
-
- context 'when async_merge_request_diff_creation is disabled' do
- before do
- stub_feature_flags(async_merge_request_diff_creation: false)
- end
-
- it 'creates the merge request' do
- expect_next_instance_of(MergeRequest) do |instance|
- expect(instance).to receive(:eager_fetch_ref!).and_call_original
- end
-
- merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+ it 'creates the merge request', :sidekiq_inline do
+ merge_request = described_class.new(project: project, current_user: user, params: opts).execute
- expect(merge_request).to be_persisted
- expect(merge_request.iid).to be > 0
- expect(merge_request.merge_request_diff).not_to be_empty
- end
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
end
it 'does not create the merge request when the target project is archived' do
diff --git a/spec/services/merge_requests/export_csv_service_spec.rb b/spec/services/merge_requests/export_csv_service_spec.rb
index 97217e979a5..2f0036845e7 100644
--- a/spec/services/merge_requests/export_csv_service_spec.rb
+++ b/spec/services/merge_requests/export_csv_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::ExportCsvService do
+RSpec.describe MergeRequests::ExportCsvService, feature_category: :importers do
let_it_be(:merge_request) { create(:merge_request) }
let(:csv) { CSV.parse(subject.csv_data, headers: true).first }
@@ -113,5 +113,21 @@ RSpec.describe MergeRequests::ExportCsvService do
end
end
end
+
+ describe '#email' do
+ let_it_be(:user) { create(:user) }
+
+ it 'emails csv' do
+ expect { subject.email(user) }.to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it 'renders with a target filesize' do
+ expect_next_instance_of(CsvBuilder) do |csv_builder|
+ expect(csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE).once
+ end
+
+ subject.email(user)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/link_lfs_objects_service_spec.rb b/spec/services/merge_requests/link_lfs_objects_service_spec.rb
index 96cb72baac2..9762b600eab 100644
--- a/spec/services/merge_requests/link_lfs_objects_service_spec.rb
+++ b/spec/services/merge_requests/link_lfs_objects_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline do
+RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline, feature_category: :code_review_workflow do
include ProjectForksHelper
include RepoHelpers
diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb
index 59424263ec5..cb5d0a6bd25 100644
--- a/spec/services/merge_requests/pushed_branches_service_spec.rb
+++ b/spec/services/merge_requests/pushed_branches_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::PushedBranchesService do
+RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_code_management do
let(:project) { create(:project) }
let!(:service) { described_class.new(project: project, current_user: nil, params: { changes: pushed_branches }) }
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 316f20d8276..704dc1f9000 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RebaseService do
+RSpec.describe MergeRequests::RebaseService, feature_category: :source_code_management do
include ProjectForksHelper
let(:user) { create(:user) }
diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb
index fd8240935e8..e4e54db5013 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RemoveApprovalService do
+RSpec.describe MergeRequests::RemoveApprovalService, feature_category: :code_review_workflow do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb
index 187dd0cf589..ef8cd0a861e 100644
--- a/spec/services/merge_requests/retarget_chain_service_spec.rb
+++ b/spec/services/merge_requests/retarget_chain_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::RetargetChainService do
+RSpec.describe MergeRequests::RetargetChainService, feature_category: :code_review_workflow do
include ProjectForksHelper
let_it_be(:user) { create(:user) }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 344d93fc5ca..e20ebf18e7c 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -196,7 +196,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_milestone_changed_action).once.with(user: user)
- opts[:milestone] = milestone
+ opts[:milestone_id] = milestone.id
MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request)
end
@@ -236,27 +236,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
end
context 'updating milestone' do
- RSpec.shared_examples 'updates milestone' do
+ context 'with milestone_id param' do
+ let(:opts) { { milestone_id: milestone.id } }
+
it 'sets milestone' do
expect(@merge_request.milestone).to eq milestone
end
end
- context 'when milestone_id param' do
- let(:opts) { { milestone_id: milestone.id } }
-
- it_behaves_like 'updates milestone'
- end
-
- context 'when milestone param' do
- let(:opts) { { milestone: milestone } }
-
- it_behaves_like 'updates milestone'
- end
-
context 'milestone counters cache reset' do
let(:milestone_old) { create(:milestone, project: project) }
- let(:opts) { { milestone: milestone_old } }
+ let(:opts) { { milestone_id: milestone_old.id } }
it 'deletes milestone counters' do
expect_next_instance_of(Milestones::MergeRequestsCountService, milestone_old) do |service|
@@ -267,7 +257,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: milestone)
+ update_merge_request(milestone_id: milestone.id)
end
it 'deletes milestone counters when the milestone is removed' do
@@ -275,17 +265,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: nil)
+ update_merge_request(milestone_id: nil)
end
it 'deletes milestone counters when the milestone was not set' do
- update_merge_request(milestone: nil)
+ update_merge_request(milestone_id: nil)
expect_next_instance_of(Milestones::MergeRequestsCountService, milestone) do |service|
expect(service).to receive(:delete_cache).and_call_original
end
- update_merge_request(milestone: milestone)
+ update_merge_request(milestone_id: milestone.id)
end
end
end
@@ -754,12 +744,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
expect(service).to receive(:async_execute)
end
- update_merge_request({ milestone: create(:milestone, project: project) })
+ update_merge_request(milestone_id: create(:milestone, project: project).id)
end
it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
- update_merge_request(milestone: create(:milestone, project: project))
+ update_merge_request(milestone_id: create(:milestone, project: project).id)
end
should_email(subscriber)
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 22606cc2461..1ee9e51433e 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Notes::CreateService do
+RSpec.describe Notes::CreateService, feature_category: :team_planning do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
@@ -116,6 +116,35 @@ RSpec.describe Notes::CreateService do
end
end
+ context 'in a commit', :snowplow do
+ let_it_be(:commit) { create(:commit, project: project) }
+ let(:opts) { { note: 'Awesome comment', noteable_type: 'Commit', commit_id: commit.id } }
+
+ let(:counter) { Gitlab::UsageDataCounters::NoteCounter }
+
+ let(:execute_create_service) { described_class.new(project, user, opts).execute }
+
+ before do
+ stub_feature_flags(notes_create_service_tracking: false)
+ end
+
+ it 'tracks commit comment usage data', :clean_gitlab_redis_shared_state do
+ expect(counter).to receive(:count).with(:create, 'Commit').and_call_original
+
+ expect do
+ execute_create_service
+ end.to change { counter.read(:create, 'Commit') }.by(1)
+ end
+
+ it_behaves_like 'Snowplow event tracking with Redis context' do
+ let(:category) { described_class.name }
+ let(:action) { 'create_commit_comment' }
+ let(:label) { 'counts.commit_comment' }
+ let(:namespace) { project.namespace }
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase4 }
+ end
+ end
+
describe 'event tracking', :snowplow do
let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED }
let(:execute_create_service) { described_class.new(project, user, opts).execute }
@@ -409,7 +438,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'for merge requests' do
+ context 'for merge requests', feature_category: :code_review_workflow do
let_it_be(:merge_request) { create(:merge_request, source_project: project, labels: [bug_label]) }
let(:issuable) { merge_request }
@@ -483,7 +512,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'personal snippet note' do
+ context 'personal snippet note', feature_category: :source_code_management do
subject { described_class.new(nil, user, params).execute }
let(:snippet) { create(:personal_snippet) }
@@ -504,7 +533,7 @@ RSpec.describe Notes::CreateService do
end
end
- context 'design note' do
+ context 'design note', feature_category: :design_management do
subject(:service) { described_class.new(project, user, params) }
let_it_be(:design) { create(:design, :with_file) }
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 82caec52aee..744808525f5 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -91,5 +91,13 @@ RSpec.describe Notes::DestroyService do
end
end
end
+
+ it 'tracks design comment removal' do
+ note = create(:note_on_design, project: project)
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_design_comment_removed_action).with(author: note.author,
+ project: project)
+
+ described_class.new(project, user).execute(note)
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 1ad9234c939..4161f93cdac 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- shared_examples 'is not able to send notifications' do
+ shared_examples 'is not able to send notifications' do |check_delivery_jobs_queue: false|
it 'does not send any notification' do
user_1 = create(:user)
recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release)
@@ -107,12 +107,21 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
expect(Gitlab::AppLogger).to receive(:warn).with(message: 'Skipping sending notifications', user: current_user.id, klass: object.class.to_s, object_id: object.id)
- action
+ if check_delivery_jobs_queue
+ expect do
+ action
+ end.to not_enqueue_mail_with(Notify, notification_method, @u_mentioned, anything, anything)
+ .and(not_enqueue_mail_with(Notify, notification_method, @u_guest_watcher, anything, anything))
+ .and(not_enqueue_mail_with(Notify, notification_method, user_1, anything, anything))
+ .and(not_enqueue_mail_with(Notify, notification_method, current_user, anything, anything))
+ else
+ action
- should_not_email(@u_mentioned)
- should_not_email(@u_guest_watcher)
- should_not_email(user_1)
- should_not_email(current_user)
+ should_not_email(@u_mentioned)
+ should_not_email(@u_guest_watcher)
+ should_not_email(user_1)
+ should_not_email(current_user)
+ end
end
end
@@ -123,13 +132,19 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
# * notification trigger
# * participant
#
- shared_examples 'participating by note notification' do
+ shared_examples 'participating by note notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: participant)
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
context 'for subgroups' do
@@ -140,14 +155,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
it 'emails the participant' do
create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant)
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(@pg_participant))
+ else
+ notification_trigger
- should_email_nested_group_user(@pg_participant)
+ should_email_nested_group_user(@pg_participant)
+ end
end
end
end
- shared_examples 'participating by confidential note notification' do
+ shared_examples 'participating by confidential note notification' do |check_delivery_jobs_queue: false|
context 'when user is mentioned on confidential note' do
let_it_be(:guest_1) { create(:user) }
let_it_be(:guest_2) { create(:user) }
@@ -164,34 +185,55 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
note_text = "Mentions #{guest_2.to_reference}"
create(:note_on_issue, noteable: issuable, project_id: project.id, note: confidential_note_text, confidential: true)
create(:note_on_issue, noteable: issuable, project_id: project.id, note: note_text)
- reset_delivered_emails!
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_2))
+ .and(enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(reporter)))
+ .and(not_enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_1)))
+ else
+ reset_delivered_emails!
+
+ notification_trigger
- should_not_email(guest_1)
- should_email(guest_2)
- should_email(reporter)
+ should_not_email(guest_1)
+ should_email(guest_2)
+ should_email(reporter)
+ end
end
end
end
- shared_examples 'participating by assignee notification' do
+ shared_examples 'participating by assignee notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
issuable.assignees << participant
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
end
- shared_examples 'participating by author notification' do
+ shared_examples 'participating by author notification' do |check_delivery_jobs_queue: false|
it 'emails the participant' do
issuable.author = participant
- notification_trigger
+ if check_delivery_jobs_queue
+ expect do
+ notification_trigger
+ end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant))
+ else
+ notification_trigger
- should_email(participant)
+ should_email(participant)
+ end
end
end
@@ -205,10 +247,10 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- shared_examples_for 'participating notifications' do
- it_behaves_like 'participating by note notification'
- it_behaves_like 'participating by author notification'
- it_behaves_like 'participating by assignee notification'
+ shared_examples_for 'participating notifications' do |check_delivery_jobs_queue: false|
+ it_behaves_like 'participating by note notification', check_delivery_jobs_queue: check_delivery_jobs_queue
+ it_behaves_like 'participating by author notification', check_delivery_jobs_queue: check_delivery_jobs_queue
+ it_behaves_like 'participating by assignee notification', check_delivery_jobs_queue: check_delivery_jobs_queue
end
describe '.permitted_actions' do
@@ -1159,7 +1201,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe 'Issues', :deliver_mails_inline do
+ describe 'Issues', :aggregate_failures do
let(:another_project) { create(:project, :public, namespace: group) }
let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
@@ -1184,79 +1226,77 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
describe '#new_issue' do
it 'notifies the expected users' do
- notification.new_issue(issue, @u_disabled)
-
- should_email(assignee)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_custom_global)
- should_email(@u_participant_mentioned)
- should_email(@g_global_watcher)
- should_email(@g_watcher)
- should_email(@unsubscribed_mentioned)
- should_email_nested_group_user(@pg_watcher)
- should_not_email(@u_mentioned)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
- should_not_email_nested_group_user(@pg_disabled)
- should_not_email_nested_group_user(@pg_mention)
- end
-
- it do
- create_global_setting_for(issue.assignees.first, :mention)
- notification.new_issue(issue, @u_disabled)
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_custom, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_custom_global, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_participant_mentioned, issue, 'mentioned'))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @g_global_watcher.id, issue.id, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @g_watcher, issue, nil))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @unsubscribed_mentioned, issue, 'mentioned'))
+ .and(enqueue_mail_with(Notify, :new_issue_email, @pg_watcher, issue, nil))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_participating, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_disabled, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_lazy_participant, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_disabled, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_mention, anything, anything))
+ end
+
+ context 'when user has an only mention notification setting' do
+ before do
+ create_global_setting_for(issue.assignees.first, :mention)
+ end
- should_not_email(issue.assignees.first)
+ it 'does not send assignee notifications' do
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.assignees.first, anything, anything)
+ end
end
it 'properly prioritizes notification reason' do
# have assignee be both assigned and mentioned
issue.update_attribute(:description, "/cc #{assignee.to_reference} #{@u_mentioned.to_reference}")
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', 'assigned')
-
- email = find_email_for(@u_mentioned)
- expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
+ .and(enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned'))
end
it 'adds "assigned" reason for assignees if any' do
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(assignee)
-
- expect(email).to have_header('X-GitLab-NotificationReason', 'assigned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned')
end
it "emails any mentioned users with the mention level" do
issue.description = @u_mentioned.to_reference
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(@u_mentioned)
- expect(email).not_to be_nil
- expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned')
+ expect do
+ notification.new_issue(issue, @u_disabled)
+ end.to enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned')
end
it "emails the author if they've opted into notifications about their activity" do
issue.author.notified_of_own_activity = true
- notification.new_issue(issue, issue.author)
-
- should_email(issue.author)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, issue.author, issue, 'own_activity')
end
it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_issue(issue, issue.author)
-
- should_not_email(issue.author)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.author, anything, anything)
end
- it "emails subscribers of the issue's labels" do
+ it "emails subscribers of the issue's labels and adds `subscribed` reason" do
user_1 = create(:user)
user_2 = create(:user)
user_3 = create(:user)
@@ -1269,27 +1309,15 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
group_label.toggle_subscription(user_3, another_project)
group_label.toggle_subscription(user_4)
- notification.new_issue(issue, @u_disabled)
-
- should_email(user_1)
- should_email(user_2)
- should_not_email(user_3)
- should_email(user_4)
- end
-
- it 'adds "subscribed" reason to subscriber emails' do
- user_1 = create(:user)
- label = create(:label, project: project, issues: [issue])
- issue.reload
- label.subscribe(user_1)
-
- notification.new_issue(issue, @u_disabled)
-
- email = find_email_for(user_1)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::SUBSCRIBED)
+ expect do
+ notification.new_issue(issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, user_1, issue, NotificationReason::SUBSCRIBED)
+ .and(enqueue_mail_with(Notify, :new_issue_email, user_2, issue, NotificationReason::SUBSCRIBED))
+ .and(enqueue_mail_with(Notify, :new_issue_email, user_4, issue, NotificationReason::SUBSCRIBED))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, user_3, anything, anything))
end
- it_behaves_like 'project emails are disabled' do
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { notification.new_issue(issue, @u_disabled) }
end
@@ -1315,35 +1343,33 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
label.toggle_subscription(guest, project)
label.toggle_subscription(admin, project)
- reset_delivered_emails!
-
- notification.new_issue(confidential_issue, @u_disabled)
-
- should_not_email(@u_guest_watcher)
- should_not_email(non_member)
- should_not_email(author)
- should_not_email(guest)
- should_email(assignee)
- should_email(member)
- should_email(admin)
+ expect do
+ notification.new_issue(confidential_issue, issue.author)
+ end.to enqueue_mail_with(Notify, :new_issue_email, assignee, confidential_issue, NotificationReason::ASSIGNED)
+ .and(enqueue_mail_with(Notify, :new_issue_email, member, confidential_issue, NotificationReason::SUBSCRIBED))
+ .and(enqueue_mail_with(Notify, :new_issue_email, admin, confidential_issue, NotificationReason::SUBSCRIBED))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, non_member, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, author, anything, anything))
+ .and(not_enqueue_mail_with(Notify, :new_issue_email, guest, anything, anything))
end
end
context 'when the author is not allowed to trigger notifications' do
- let(:current_user) { nil }
let(:object) { issue }
let(:action) { notification.new_issue(issue, current_user) }
+ let(:notification_method) { :new_issue_email }
context 'because they are blocked' do
let(:current_user) { create(:user, :blocked) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
context 'because they are a ghost' do
let(:current_user) { create(:user, :ghost) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
end
end
@@ -1354,9 +1380,52 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
let(:object) { mentionable }
let(:action) { send_notifications(@u_mentioned, current_user: current_user) }
- include_examples 'notifications for new mentions'
+ it 'sends no emails when no new mentions are present' do
+ send_notifications
- it_behaves_like 'project emails are disabled' do
+ expect_no_delivery_jobs
+ end
+
+ it 'emails new mentions with a watch level higher than mention' do
+ expect do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@u_watcher.id, mentionable.id, anything, anything],
+ [@u_participant_mentioned.id, mentionable.id, anything, anything],
+ [@u_custom_global.id, mentionable.id, anything, anything],
+ [@u_mentioned.id, mentionable.id, anything, anything]
+ )
+ end
+
+ it 'does not email new mentions with a watch level equal to or less than mention' do
+ send_notifications(@u_disabled)
+
+ expect_no_delivery_jobs
+ end
+
+ it 'emails new mentions despite being unsubscribed' do
+ expect do
+ send_notifications(@unsubscribed_mentioned)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@unsubscribed_mentioned.id, mentionable.id, anything, anything]
+ )
+ end
+
+ it 'sends the proper notification reason header' do
+ expect do
+ send_notifications(@u_watcher)
+ end.to have_only_enqueued_mail_with_args(
+ Notify,
+ :new_mention_in_issue_email,
+ [@u_watcher.id, mentionable.id, anything, NotificationReason::MENTIONED]
+ )
+ end
+
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) }
end
@@ -1364,117 +1433,130 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
context 'where current_user is blocked' do
let(:current_user) { create(:user, :blocked) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
context 'where current_user is a ghost' do
let(:current_user) { create(:user, :ghost) }
- include_examples 'is not able to send notifications'
+ include_examples 'is not able to send notifications', check_delivery_jobs_queue: true
end
end
describe '#reassigned_issue' do
+ let(:anything_args) { [anything, anything, anything, anything] }
+ let(:mailer_method) { :reassigned_issue_email }
+
before do
update_custom_notification(:reassign_issue, @u_guest_custom, resource: project)
update_custom_notification(:reassign_issue, @u_custom_global)
end
it 'emails new assignee' do
- notification.reassigned_issue(issue, @u_disabled, [assignee])
-
- should_email(issue.assignees.first)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_custom_global)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [assignee])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_issue(issue, @u_disabled, [assignee])
-
- email = find_email_for(assignee)
-
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [assignee])
+ end.to enqueue_mail_with(
+ Notify,
+ :reassigned_issue_email,
+ issue.assignees.first,
+ anything,
+ anything,
+ anything,
+ NotificationReason::ASSIGNED
+ )
end
it 'emails previous assignee even if they have the "on mention" notif level' do
issue.assignees = [@u_mentioned]
- notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
- should_email(@u_mentioned)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [@u_watcher])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_mentioned, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'emails new assignee even if they have the "on mention" notif level' do
issue.assignees = [@u_mentioned]
- notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
- expect(issue.assignees.first).to be @u_mentioned
- should_email(issue.assignees.first)
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ expect(issue.assignees.first).to eq(@u_mentioned)
+ expect do
+ notification.reassigned_issue(issue, @u_disabled, [@u_mentioned])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
end
it 'does not email new assignee if they are the current user' do
issue.assignees = [@u_mentioned]
notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
- expect(issue.assignees.first).to be @u_mentioned
- should_email(@u_watcher)
- should_email(@u_guest_watcher)
- should_email(@u_guest_custom)
- should_email(@u_participant_mentioned)
- should_email(@subscriber)
- should_email(@u_custom_global)
- should_not_email(issue.assignees.first)
- should_not_email(@unsubscriber)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
- end
-
- it_behaves_like 'participating notifications' do
+ expect(issue.assignees.first).to eq(@u_mentioned)
+ expect do
+ notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned])
+ end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args)
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args))
+ .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args))
+ .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args))
+ end
+
+ it_behaves_like 'participating notifications', check_delivery_jobs_queue: true do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
- it_behaves_like 'participating by confidential note notification' do
+ it_behaves_like 'participating by confidential note notification', check_delivery_jobs_queue: true do
let(:issuable) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
- it_behaves_like 'project emails are disabled' do
+ it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do
let(:notification_target) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
end
- describe '#relabeled_issue' do
+ describe '#relabeled_issue', :deliver_mails_inline do
let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', issues: [issue]) }
let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
let(:label_1) { create(:label, project: project, title: 'Label 1', issues: [issue]) }
@@ -1571,25 +1653,25 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#removed_milestone_issue' do
+ describe '#removed_milestone on Issue', :deliver_mails_inline do
context do
let(:milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on issue' do
before do
- notification.removed_milestone_issue(issue, issue.author)
+ notification.removed_milestone(issue, issue.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { issue }
- let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) }
+ let(:notification_trigger) { notification.removed_milestone(issue, issue.author) }
end
it_behaves_like 'participating by confidential note notification' do
let(:issuable) { issue }
- let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) }
+ let(:notification_trigger) { notification.removed_milestone(issue, issue.author) }
end
end
@@ -1615,7 +1697,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
reset_delivered_emails!
- notification.removed_milestone_issue(confidential_issue, @u_disabled)
+ notification.removed_milestone(confidential_issue, @u_disabled)
should_not_email(non_member)
should_not_email(guest)
@@ -1627,20 +1709,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#changed_milestone_issue' do
+ describe '#changed_milestone on Issue', :deliver_mails_inline do
context do
let(:new_milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on issue' do
before do
- notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ notification.changed_milestone(issue, new_milestone, issue.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { issue }
- let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) }
+ let(:notification_trigger) { notification.changed_milestone(issue, new_milestone, issue.author) }
end
end
@@ -1666,7 +1748,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
reset_delivered_emails!
- notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled)
+ notification.changed_milestone(confidential_issue, new_milestone, @u_disabled)
should_not_email(non_member)
should_not_email(guest)
@@ -1678,7 +1760,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#close_issue' do
+ describe '#close_issue', :deliver_mails_inline do
before do
update_custom_notification(:close_issue, @u_guest_custom, resource: project)
update_custom_notification(:close_issue, @u_custom_global)
@@ -1730,7 +1812,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#reopen_issue' do
+ describe '#reopen_issue', :deliver_mails_inline do
before do
update_custom_notification(:reopen_issue, @u_guest_custom, resource: project)
update_custom_notification(:reopen_issue, @u_custom_global)
@@ -1771,7 +1853,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_moved' do
+ describe '#issue_moved', :deliver_mails_inline do
let(:new_issue) { create(:issue) }
it 'sends email to issue notification recipients' do
@@ -1807,7 +1889,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_cloned' do
+ describe '#issue_cloned', :deliver_mails_inline do
let(:new_issue) { create(:issue) }
it 'sends email to issue notification recipients' do
@@ -1843,7 +1925,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#issue_due' do
+ describe '#issue_due', :deliver_mails_inline do
before do
issue.update!(due_date: Date.today)
@@ -2395,35 +2477,35 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
end
end
- describe '#removed_milestone_merge_request' do
+ describe '#removed_milestone on MergeRequest' do
let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on merge request' do
before do
- notification.removed_milestone_merge_request(merge_request, merge_request.author)
+ notification.removed_milestone(merge_request, merge_request.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { merge_request }
- let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) }
+ let(:notification_trigger) { notification.removed_milestone(merge_request, merge_request.author) }
end
end
- describe '#changed_milestone_merge_request' do
+ describe '#changed_milestone on MergeRequest' do
let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
it_behaves_like 'altered milestone notification on merge request' do
before do
- notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author)
+ notification.changed_milestone(merge_request, new_milestone, merge_request.author)
end
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { merge_request }
- let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) }
+ let(:notification_trigger) { notification.changed_milestone(merge_request, new_milestone, merge_request.author) }
end
end
@@ -3920,4 +4002,8 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
# Make the watcher a subscriber to detect dupes
issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true)
end
+
+ def expectation_args_for_user(user)
+ [user, *anything_args]
+ end
end
diff --git a/spec/services/packages/debian/create_distribution_service_spec.rb b/spec/services/packages/debian/create_distribution_service_spec.rb
index ecf82c6a1db..1c53f75cfb6 100644
--- a/spec/services/packages/debian/create_distribution_service_spec.rb
+++ b/spec/services/packages/debian/create_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::CreateDistributionService do
+RSpec.describe Packages::Debian::CreateDistributionService, feature_category: :package_registry do
RSpec.shared_examples 'Create Debian Distribution' do |expected_message, expected_components, expected_architectures|
let_it_be(:container) { create(container_type) } # rubocop:disable Rails/SaveBang
diff --git a/spec/services/packages/debian/create_package_file_service_spec.rb b/spec/services/packages/debian/create_package_file_service_spec.rb
index 291f6df991c..43928669eb1 100644
--- a/spec/services/packages/debian/create_package_file_service_spec.rb
+++ b/spec/services/packages/debian/create_package_file_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::CreatePackageFileService do
+RSpec.describe Packages::Debian::CreatePackageFileService, feature_category: :package_registry do
include WorkhorseHelpers
let_it_be(:package) { create(:debian_incoming, without_package_files: true) }
@@ -11,8 +11,9 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
describe '#execute' do
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
let(:fixture_path) { "spec/fixtures/packages/debian/#{file_name}" }
+ let(:params) { default_params }
- let(:params) do
+ let(:default_params) do
{
file: file,
file_name: file_name,
@@ -25,8 +26,15 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
subject(:package_file) { service.execute }
- shared_examples 'a valid deb' do
+ shared_examples 'a valid deb' do |process_package_file_worker|
it 'creates a new package file', :aggregate_failures do
+ if process_package_file_worker
+ expect(::Packages::Debian::ProcessPackageFileWorker)
+ .to receive(:perform_async).with(an_instance_of(Integer), params[:distribution], params[:component])
+ else
+ expect(::Packages::Debian::ProcessPackageFileWorker).not_to receive(:perform_async)
+ end
+
expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
expect(package_file).to be_valid
expect(package_file.file.read).to start_with('!<arch>')
@@ -44,7 +52,8 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
shared_examples 'a valid changes' do
it 'creates a new package file', :aggregate_failures do
- expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async)
+ expect(::Packages::Debian::ProcessChangesWorker)
+ .to receive(:perform_async).with(an_instance_of(Integer), current_user.id)
expect(package_file).to be_valid
expect(package_file.file.read).to start_with('Format: 1.8')
@@ -80,6 +89,12 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
it_behaves_like 'a valid changes'
end
+ context 'with distribution' do
+ let(:params) { default_params.merge(distribution: 'unstable', component: 'main') }
+
+ it_behaves_like 'a valid deb', true
+ end
+
context 'when current_user is missing' do
let(:current_user) { nil }
@@ -137,13 +152,5 @@ RSpec.describe Packages::Debian::CreatePackageFileService do
expect { package_file }.to raise_error(ActiveRecord::RecordInvalid)
end
end
-
- context 'when FIPS mode enabled', :fips_mode do
- let(:file) { nil }
-
- it 'raises an error' do
- expect { package_file }.to raise_error(::Packages::FIPS::DisabledError)
- end
- end
end
end
diff --git a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
index 4765e6c3bd4..4d6acac219b 100644
--- a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
@@ -1,10 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractChangesMetadataService do
+RSpec.describe Packages::Debian::ExtractChangesMetadataService, feature_category: :package_registry do
describe '#execute' do
- let_it_be(:distribution) { create(:debian_project_distribution, codename: 'unstable') }
- let_it_be(:incoming) { create(:debian_incoming, project: distribution.project) }
+ let_it_be(:incoming) { create(:debian_incoming) }
let(:source_file) { incoming.package_files.first }
let(:dsc_file) { incoming.package_files.second }
@@ -13,12 +12,6 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
subject { service.execute }
- 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 valid package file' do
it 'extract metadata', :aggregate_failures do
expected_fields = { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' }
diff --git a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
index 66a9ca5f9e0..1f5cf2ace5a 100644
--- a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractDebMetadataService do
+RSpec.describe Packages::Debian::ExtractDebMetadataService, feature_category: :package_registry do
subject { described_class.new(file_path) }
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb
index 02c81ad1644..412f285152b 100644
--- a/spec/services/packages/debian/extract_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_metadata_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ExtractMetadataService do
+RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :package_registry do
let(:service) { described_class.new(package_file) }
subject { service.execute }
diff --git a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
index e1393c774b1..27c389b5312 100644
--- a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
+++ b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::FindOrCreateIncomingService do
+RSpec.describe Packages::Debian::FindOrCreateIncomingService, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
diff --git a/spec/services/packages/debian/find_or_create_package_service_spec.rb b/spec/services/packages/debian/find_or_create_package_service_spec.rb
index 84a0e1465e8..36f96008582 100644
--- a/spec/services/packages/debian/find_or_create_package_service_spec.rb
+++ b/spec/services/packages/debian/find_or_create_package_service_spec.rb
@@ -2,54 +2,57 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::FindOrCreatePackageService do
- let_it_be(:distribution) { create(:debian_project_distribution) }
+RSpec.describe Packages::Debian::FindOrCreatePackageService, feature_category: :package_registry do
+ let_it_be(:distribution) { create(:debian_project_distribution, :with_suite) }
let_it_be(:project) { distribution.project }
let_it_be(:user) { create(:user) }
- let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
+ let(:service) { described_class.new(project, user, params) }
- subject(:service) { described_class.new(project, user, params) }
+ let(:package) { subject.payload[:package] }
+ let(:package2) { service.execute.payload[:package] }
- describe '#execute' do
- subject { service.execute }
+ shared_examples 'find or create Debian package' do
+ it 'returns the same object' do
+ expect { subject }.to change { ::Packages::Package.count }.by(1)
+ expect(subject).to be_success
+ expect(package).to be_valid
+ expect(package.project_id).to eq(project.id)
+ expect(package.creator_id).to eq(user.id)
+ expect(package.name).to eq('foo')
+ expect(package.version).to eq('1.0+debian')
+ expect(package).to be_debian
+ expect(package.debian_publication.distribution).to eq(distribution)
- let(:package) { subject.payload[:package] }
+ expect { package2 }.not_to change { ::Packages::Package.count }
+ expect(package2.id).to eq(package.id)
+ end
- context 'run once' do
- it 'creates a new package', :aggregate_failures do
+ context 'with package marked as pending_destruction' do
+ it 'creates a new package' do
expect { subject }.to change { ::Packages::Package.count }.by(1)
- expect(subject).to be_success
-
- expect(package).to be_valid
- expect(package.project_id).to eq(project.id)
- expect(package.creator_id).to eq(user.id)
- expect(package.name).to eq('foo')
- expect(package.version).to eq('1.0+debian')
- expect(package).to be_debian
- expect(package.debian_publication.distribution).to eq(distribution)
+
+ package.pending_destruction!
+
+ expect { package2 }.to change { ::Packages::Package.count }.by(1)
+ expect(package2.id).not_to eq(package.id)
end
end
+ end
- context 'run twice' do
- let(:package2) { service.execute.payload[:package] }
+ describe '#execute' do
+ subject { service.execute }
- it 'returns the same object' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
- expect { package2 }.not_to change { ::Packages::Package.count }
+ context 'with a codename as distribution name' do
+ let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } }
- expect(package2.id).to eq(package.id)
- end
+ it_behaves_like 'find or create Debian package'
+ end
- context 'with package marked as pending_destruction' do
- it 'creates a new package' do
- expect { subject }.to change { ::Packages::Package.count }.by(1)
- package.pending_destruction!
- expect { package2 }.to change { ::Packages::Package.count }.by(1)
+ context 'with a suite as distribution name' do
+ let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.suite } }
- expect(package2.id).not_to eq(package.id)
- end
- end
+ it_behaves_like 'find or create Debian package'
end
context 'with non-existing distribution' do
diff --git a/spec/services/packages/debian/generate_distribution_key_service_spec.rb b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
index f82d577f071..bc86a9592d0 100644
--- a/spec/services/packages/debian/generate_distribution_key_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GenerateDistributionKeyService do
+RSpec.describe Packages::Debian::GenerateDistributionKeyService, feature_category: :package_registry do
let(:params) { {} }
subject { described_class.new(params: params) }
diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb
index fe5fbfbbe1f..6d179c791a3 100644
--- a/spec/services/packages/debian/generate_distribution_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GenerateDistributionService do
+RSpec.describe Packages::Debian::GenerateDistributionService, feature_category: :package_registry do
describe '#execute' do
subject { described_class.new(distribution).execute }
@@ -15,12 +15,6 @@ RSpec.describe Packages::Debian::GenerateDistributionService do
context "for #{container_type}" do
include_context 'with Debian distribution', container_type
- context 'with FIPS mode enabled', :fips_mode do
- it 'raises an error' do
- expect { subject }.to raise_error(::Packages::FIPS::DisabledError)
- end
- end
-
it_behaves_like 'Generate Debian Distribution and component files'
end
end
diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb
index a2731816459..35b7ead9209 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ParseDebian822Service do
+RSpec.describe Packages::Debian::ParseDebian822Service, feature_category: :package_registry do
subject { described_class.new(input) }
context 'with dpkg-deb --field output' do
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index 27b49a13d52..e3ed744377e 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ProcessChangesService do
+RSpec.describe Packages::Debian::ProcessChangesService, feature_category: :package_registry do
describe '#execute' do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, suite: 'unstable') }
let!(:incoming) { create(:debian_incoming, project: distribution.project) }
- let(:package_file) { incoming.package_files.last }
+ let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first }
subject { described_class.new(package_file, user) }
@@ -27,11 +27,37 @@ RSpec.describe Packages::Debian::ProcessChangesService do
expect(created_package.creator).to eq user
end
- context 'with existing package' do
- let_it_be_with_reload(:existing_package) { create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project) }
-
+ context 'with non-matching distribution' do
before do
- existing_package.update!(debian_distribution: distribution)
+ distribution.update! suite: FFaker::Lorem.word
+ end
+
+ it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+
+ context 'with missing field in .changes file' do
+ shared_examples 'raises error with missing field' do |missing_field|
+ before do
+ allow_next_instance_of(::Packages::Debian::ExtractChangesMetadataService) do |extract_changes_metadata_service|
+ expect(extract_changes_metadata_service).to receive(:execute).once.and_wrap_original do |m, *args|
+ metadata = m.call(*args)
+ metadata[:fields].delete(missing_field)
+ metadata
+ end
+ end
+ end
+
+ it { expect { subject.execute }.to raise_error(ArgumentError, "missing #{missing_field} field") }
+ end
+
+ it_behaves_like 'raises error with missing field', 'Source'
+ it_behaves_like 'raises error with missing field', 'Version'
+ it_behaves_like 'raises error with missing field', 'Distribution'
+ end
+
+ context 'with existing package' do
+ let_it_be_with_reload(:existing_package) do
+ create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project, published_in: distribution)
end
it 'does not create a package and assigns the package_file to the existing package' do
diff --git a/spec/services/packages/debian/process_package_file_service_spec.rb b/spec/services/packages/debian/process_package_file_service_spec.rb
index 571861f42cf..caf29cfc4fa 100644
--- a/spec/services/packages/debian/process_package_file_service_spec.rb
+++ b/spec/services/packages/debian/process_package_file_service_spec.rb
@@ -1,20 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Debian::ProcessPackageFileService do
+RSpec.describe Packages::Debian::ProcessPackageFileService, feature_category: :package_registry do
describe '#execute' do
- let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') }
-
- let!(:incoming) { create(:debian_incoming, project: distribution.project) }
+ let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_suite, :with_file) }
+ let!(:package) { create(:debian_package, :processing, project: distribution.project, published_in: nil) }
let(:distribution_name) { distribution.codename }
+ let(:component_name) { 'main' }
let(:debian_file_metadatum) { package_file.debian_file_metadatum }
- subject { described_class.new(package_file, user, distribution_name, component_name) }
+ subject { described_class.new(package_file, 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 }
+ shared_examples 'updates package and package file' do
+ it 'updates package and package file', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker)
+ .to receive(:perform_async).with(:project, distribution.id)
+ expect { subject.execute }
+ .to not_change(Packages::Package, :count)
+ .and not_change(Packages::PackageFile, :count)
+ .and change(Packages::Debian::Publication, :count).by(1)
+ .and not_change(package.package_files, :count)
+ .and change { package.reload.name }.to('sample')
+ .and change { package.reload.version }.to('1.2.3~alpha2')
+ .and change { package.reload.status }.from('processing').to('default')
+ .and change { package.reload.debian_publication }.from(nil)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
+ .and change(debian_file_metadatum, :component).from(nil).to(component_name)
+ end
end
using RSpec::Parameterized::TableSyntax
@@ -25,59 +38,68 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
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 Debian package file' do
+ let(:package_file) { package.package_files.with_file_name(file_name).first }
- 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)
+ context 'when there is no matching published package' do
+ it_behaves_like 'updates package and package file'
+
+ context 'with suite as distribution name' do
+ let(:distribution_name) { distribution.suite }
+
+ it_behaves_like 'updates package and package file'
end
+ end
- before do
- existing_package.update!(debian_distribution: distribution)
+ context 'when there is a matching published package' do
+ let!(:matching_package) do
+ create(
+ :debian_package,
+ project: distribution.project,
+ published_in: distribution,
+ name: 'sample',
+ version: '1.2.3~alpha2'
+ )
end
- it 'does not create a package and assigns the package_file to the existing package' do
+ it 'reuses existing package and update package file', :aggregate_failures 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)
+ .to change(Packages::Package, :count).from(2).to(1)
+ .and change(Packages::PackageFile, :count).from(14).to(8)
+ .and not_change(Packages::Debian::Publication, :count)
+ .and change(package.package_files, :count).from(7).to(0)
+ .and change(package_file, :package).from(package).to(matching_package)
+ .and not_change(matching_package, :name)
+ .and not_change(matching_package, :version)
+ .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type)
.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 { package.reload }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
- expect { subject.execute }
- .to change(Packages::Package, :count).by(1)
- .and not_change(Packages::PackageFile, :count)
- end
+ context 'when there is a matching published package pending destruction' do
+ let!(:matching_package) do
+ create(
+ :debian_package,
+ :pending_destruction,
+ project: distribution.project,
+ published_in: distribution,
+ name: 'sample',
+ version: '1.2.3~alpha2'
+ )
end
+
+ it_behaves_like 'updates package and package file'
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(:package_file) { package.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
let(:component_name) { 'main' }
before do
@@ -89,42 +111,41 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
expect { subject.execute }
.to not_change(Packages::Package, :count)
.and not_change(Packages::PackageFile, :count)
- .and not_change(incoming.package_files, :count)
+ .and not_change(package.package_files, :count)
.and raise_error(ActiveRecord::RecordNotFound)
end
end
- context 'with package file without Debian metadata' do
+ context 'without distribution name' do
let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
- let(:component_name) { 'main' }
+ let(:distribution_name) { '' }
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')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'missing distribution name')
end
end
- context 'with already processed package file' do
- let_it_be(:package_file) { create(:debian_package_file) }
-
- let(:component_name) { 'main' }
+ context 'without component name' do
+ let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) }
+ let(:component_name) { '' }
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')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'missing component name')
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 }
+ 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
@@ -132,29 +153,37 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do
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')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'package file without Debian metadata')
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 }
+ context 'with already processed package file' do
+ let_it_be(:package_file) { create(:debian_package_file) }
+
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
+ 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(package.package_files, :count)
+ .and raise_error(ArgumentError, 'already processed package file')
end
+ end
- it 're-raise error', :aggregate_failures do
+ context 'with invalid package file type' do
+ let(:package_file) { package.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(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+ .and not_change(package.package_files, :count)
+ .and raise_error(ArgumentError, 'invalid package file type: source')
end
end
end
diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb
index fc070b6e45e..50c34443495 100644
--- a/spec/services/packages/debian/sign_distribution_service_spec.rb
+++ b/spec/services/packages/debian/sign_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::SignDistributionService do
+RSpec.describe Packages::Debian::SignDistributionService, feature_category: :package_registry do
let_it_be(:group) { create(:group, :public) }
let(:content) { FFaker::Lorem.paragraph }
diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb
index 3dff2754cec..cfafed5841f 100644
--- a/spec/services/packages/debian/update_distribution_service_spec.rb
+++ b/spec/services/packages/debian/update_distribution_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::UpdateDistributionService do
+RSpec.describe Packages::Debian::UpdateDistributionService, feature_category: :package_registry do
RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0|
it 'returns ServiceResponse', :aggregate_failures do
expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil?
diff --git a/spec/services/pages/destroy_deployments_service_spec.rb b/spec/services/pages/destroy_deployments_service_spec.rb
index 0f8e8b6573e..0ca8cbbb681 100644
--- a/spec/services/pages/destroy_deployments_service_spec.rb
+++ b/spec/services/pages/destroy_deployments_service_spec.rb
@@ -2,28 +2,26 @@
require 'spec_helper'
-RSpec.describe Pages::DestroyDeploymentsService do
- let(:project) { create(:project) }
+RSpec.describe Pages::DestroyDeploymentsService, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
let!(:old_deployments) { create_list(:pages_deployment, 2, project: project) }
let!(:last_deployment) { create(:pages_deployment, project: project) }
let!(:newer_deployment) { create(:pages_deployment, project: project) }
let!(:deployment_from_another_project) { create(:pages_deployment) }
it 'destroys all deployments of the project' do
- expect do
- described_class.new(project).execute
- end.to change { PagesDeployment.count }.by(-4)
+ expect { described_class.new(project).execute }
+ .to change { PagesDeployment.count }.by(-4)
- expect(deployment_from_another_project.reload).to be
+ expect(deployment_from_another_project.reload).to be_persisted
end
it 'destroy only deployments older than last deployment if it is provided' do
- expect do
- described_class.new(project, last_deployment.id).execute
- end.to change { PagesDeployment.count }.by(-2)
+ expect { described_class.new(project, last_deployment.id).execute }
+ .to change { PagesDeployment.count }.by(-2)
- expect(last_deployment.reload).to be
- expect(newer_deployment.reload).to be
- expect(deployment_from_another_project.reload).to be
+ expect(last_deployment.reload).to be_persisted
+ expect(newer_deployment.reload).to be_persisted
+ expect(deployment_from_another_project.reload).to be_persisted
end
end
diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
index d058324f3bb..4348ce4a271 100644
--- a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
+++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Pages::MigrateFromLegacyStorageService do
+RSpec.describe Pages::MigrateFromLegacyStorageService, feature_category: :pages do
let(:batch_size) { 10 }
let(:mark_projects_as_not_deployed) { false }
let(:service) { described_class.new(Rails.logger, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) }
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index fe1ab6b1d58..d1bc10cfd28 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -192,4 +192,21 @@ RSpec.describe PreviewMarkdownService do
"Sets time estimate to 2y.<br>Assigns #{user.to_reference}."
end
end
+
+ context 'work item quick action types' do
+ let(:work_item) { create(:work_item, :task, project: project) }
+ let(:params) do
+ {
+ text: "/title new title",
+ target_type: 'WorkItem',
+ target_id: work_item.iid
+ }
+ end
+
+ let(:result) { described_class.new(project, user, params).execute }
+
+ it 'renders the quick action preview' do
+ expect(result[:commands]).to eq "Changes the title to \"new title\"."
+ end
+ 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 0ec0aecaa04..fed1d13daa5 100644
--- a/spec/services/projects/container_repository/destroy_service_spec.rb
+++ b/spec/services/projects/container_repository/destroy_service_spec.rb
@@ -5,86 +5,131 @@ require 'spec_helper'
RSpec.describe Projects::ContainerRepository::DestroyService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) }
+ let_it_be(:params) { {} }
- subject { described_class.new(project, user) }
+ subject { described_class.new(project, user, params) }
before do
stub_container_registry_config(enabled: true)
end
- context 'when user does not have access to registry' do
- let!(:repository) { create(:container_repository, :root, project: project) }
+ shared_examples 'returning an error status with message' do |error_message|
+ it 'returns an error status' do
+ response = subject.execute(repository)
- it 'does not delete a repository' do
- expect { subject.execute(repository) }.not_to change { ContainerRepository.count }
+ expect(response).to include(status: :error, message: error_message)
end
end
- context 'when user has access to registry' do
+ shared_examples 'executing with permissions' do
+ let_it_be_with_refind(:repository) { create(:container_repository, :root, project: project) }
+
before do
- project.add_developer(user)
+ stub_container_registry_tags(repository: :any, tags: %w[latest stable])
end
- context 'when root container repository exists' do
- let!(:repository) { create(:container_repository, :root, project: project) }
+ it 'deletes the repository' do
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
+ expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
+ end
+
+ 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
before do
- stub_container_registry_tags(repository: :any, tags: %w[latest stable])
+ expect_cleanup_tags_service_with(container_repository: repository, return_status: :error)
+ allow(Gitlab::AppLogger).to receive(:error).and_call_original
end
- it 'deletes the repository' do
- expect_cleanup_tags_service_with(container_repository: repository, return_status: :success)
- expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1)
- end
+ it 'sets status as deleted_failed' do
+ subject.execute(repository)
- 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)
+ expect(repository).to be_delete_failed
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
+ it 'logs the error' do
+ subject.execute(repository)
- 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
+ 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
- subject.execute(repository)
+ it_behaves_like 'returning an error status with message', 'Deletion failed for container repository'
+ end
- 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
+ context 'when destroying the repository fails' do
+ before 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
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
+ it 'sets status as deleted_failed' do
+ subject.execute(repository)
+
+ expect(repository).to be_delete_failed
+ end
- subject.execute(repository)
+ it 'logs the error' do
+ 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
+ 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
- def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true)
- delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
+ it_behaves_like 'returning an error status with message', 'Deletion failed for container repository'
+ end
+ end
+
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
- 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)
+ it_behaves_like 'executing with permissions'
+ end
- expect(delete_tags_service).to receive(:execute).and_return(status: return_status)
- end
+ context 'when user does not have access to registry' do
+ let_it_be(:repository) { create(:container_repository, :root, project: project) }
+
+ it 'does not delete a repository' do
+ expect { subject.execute(repository) }.not_to change { ContainerRepository.count }
end
+
+ it_behaves_like 'returning an error status with message', 'Unauthorized access'
+ end
+
+ context 'when called during project deletion' do
+ let(:user) { nil }
+ let(:params) { { skip_permission_check: true } }
+
+ it_behaves_like 'executing with permissions'
+ end
+
+ context 'when there is no user' do
+ let(:user) { nil }
+ let(:repository) { create(:container_repository, :root, project: project) }
+
+ it_behaves_like 'returning an error status with message', 'Unauthorized access'
+ 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
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index f85a8eda7ee..e435db4efa6 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -163,7 +163,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects
describe 'after create actions' do
it 'invalidate personal_projects_count caches' do
- expect(user).to receive(:invalidate_personal_projects_count)
+ expect(Rails.cache).to receive(:delete).with(['users', user.id, 'personal_projects_count'])
create_project(user, opts)
end
@@ -947,6 +947,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects
end
it 'schedules authorization update for users with access to group', :sidekiq_inline do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index ff2de45661f..0689a65c2f4 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher do
+RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher, feature_category: :projects do
include ProjectForksHelper
include BatchDestroyDependentAssociationsHelper
@@ -151,10 +151,22 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
it_behaves_like 'deleting the project'
- it 'invalidates personal_project_count cache' do
- expect(user).to receive(:invalidate_personal_projects_count)
+ context 'personal projects count cache' do
+ context 'when the executor is the creator of the project itself' do
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
- destroy_project(project, user, {})
+ destroy_project(project, user, {})
+ end
+ end
+
+ context 'when the executor is the instance administrator', :enable_admin_mode do
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ destroy_project(project, create(:admin), {})
+ end
+ end
end
context 'with running pipelines' do
@@ -331,18 +343,30 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
end
context 'when image repository deletion succeeds' do
- it 'removes tags' do
- expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:execute).and_return({ status: :success })
+ it 'returns true' do
+ expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |instance|
+ expect(instance).to receive(:execute).and_return(status: :success)
+ end
- destroy_project(project, user)
+ expect(destroy_project(project, user)).to be true
+ end
+ end
+
+ context 'when image repository deletion raises an error' do
+ it 'returns false' do
+ expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |service|
+ expect(service).to receive(:execute).and_raise(RuntimeError)
+ end
+
+ expect(destroy_project(project, user)).to be false
end
end
context 'when image repository deletion fails' do
- it 'raises an exception' do
- expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:execute).and_raise(RuntimeError)
+ it 'returns false' do
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error })
+ end
expect(destroy_project(project, user)).to be false
end
@@ -369,8 +393,9 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository tags deletion succeeds' do
it 'removes tags' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :sucess })
+ end
destroy_project(project, user)
end
@@ -378,13 +403,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
context 'when image repository tags deletion fails' do
it 'raises an exception' do
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(false)
+ expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error })
+ end
expect(destroy_project(project, user)).to be false
end
end
end
+
+ context 'when there are no tags for legacy root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: [])
+ end
+
+ it 'does not try to destroy the repository' do
+ expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ destroy_project(project, user)
+ end
+ end
end
context 'for a forked project with LFS objects' do
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index 65d3085a850..eae898b4f68 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -58,6 +58,8 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
end
it 'schedules authorization update for users with access to group' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectsWorker).not_to(
receive(:bulk_perform_async)
)
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 5d07fd52230..89865d6bc3b 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -28,6 +28,8 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute' do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb
index 20616890ebd..1acbb770763 100644
--- a/spec/services/projects/group_links/update_service_spec.rb
+++ b/spec/services/projects/group_links/update_service_spec.rb
@@ -42,6 +42,8 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute' do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 2c1ebe27014..be059aec697 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe Projects::ImportExport::ExportService do
+RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do
describe '#execute' do
let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
- let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
@@ -220,5 +221,21 @@ RSpec.describe Projects::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
+
+ it "avoids N+1 when exporting project members" do
+ group.add_owner(user)
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ # warm up
+ service.execute
+
+ control = ActiveRecord::QueryRecorder.new { service.execute }
+
+ group.add_maintainer(create(:user))
+ project.add_maintainer(create(:user))
+
+ expect { service.execute }.not_to exceed_query_limit(control)
+ end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 38ab7b6e2ee..97a3b338069 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ImportService do
+RSpec.describe Projects::ImportService, feature_category: :importers do
let!(:project) { create(:project) }
let(:user) { project.creator }
diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb
index c8aa421cdd4..9f9e89ff8f8 100644
--- a/spec/services/projects/protect_default_branch_service_spec.rb
+++ b/spec/services/projects/protect_default_branch_service_spec.rb
@@ -233,6 +233,38 @@ RSpec.describe Projects::ProtectDefaultBranchService do
end
end
+ describe '#protected_branch_exists?' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:default_branch) { "default-branch" }
+
+ before do
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ create(:protected_branch, project: nil, group: group, name: default_branch)
+ end
+
+ context 'when feature flag `group_protected_branches` disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'return false' do
+ expect(service.protected_branch_exists?).to eq(false)
+ end
+ end
+
+ context 'when feature flag `group_protected_branches` enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'return true' do
+ expect(service.protected_branch_exists?).to eq(true)
+ end
+ end
+ end
+
describe '#default_branch' do
it 'returns the default branch of the project' do
allow(project)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5171836f917..32818535146 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -126,6 +126,12 @@ RSpec.describe Projects::TransferService do
expect(project.namespace).to eq(user.namespace)
end
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ execute_transfer
+ end
+
context 'the owner of the namespace does not have a direct membership in the project residing in the group' do
it 'creates a project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
execute_transfer
@@ -161,6 +167,17 @@ RSpec.describe Projects::TransferService do
end
end
+ context 'personal namespace -> group', :enable_admin_mode do
+ let(:executor) { create(:admin) }
+
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' \
+ 'that previously held the project' do
+ expect(user).to receive(:invalidate_personal_projects_count)
+
+ execute_transfer
+ end
+ end
+
context 'when transfer succeeds' do
before do
group.add_owner(user)
@@ -645,6 +662,8 @@ RSpec.describe Projects::TransferService do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
user_ids = [user.id, member_of_old_group.id, member_of_new_group.id].map { |id| [id] }
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 9c8fe769ed8..625aa4fa377 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProtectedBranches::CreateService do
+RSpec.describe ProtectedBranches::CreateService, feature_category: :compliance_management do
shared_examples 'execute with entity' do
let(:params) do
{
@@ -58,6 +58,7 @@ RSpec.describe ProtectedBranches::CreateService do
context 'with entity project' do
let_it_be_with_reload(:entity) { create(:project) }
+
let(:user) { entity.first_owner }
it_behaves_like 'execute with entity'
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 8eccb9e41bb..257e7eb972b 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe QuickActions::InterpretService do
+RSpec.describe QuickActions::InterpretService, feature_category: :team_planning do
include AfterNextHelpers
let_it_be(:group) { create(:group, :crm_enabled) }
diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb
index d960678f809..1b0a5d4ae73 100644
--- a/spec/services/quick_actions/target_service_spec.rb
+++ b/spec/services/quick_actions/target_service_spec.rb
@@ -12,9 +12,9 @@ RSpec.describe QuickActions::TargetService do
end
describe '#execute' do
- shared_examples 'no target' do |type_id:|
+ shared_examples 'no target' do |type_iid:|
it 'returns nil' do
- target = service.execute(type, type_id)
+ target = service.execute(type, type_iid)
expect(target).to be_nil
end
@@ -22,15 +22,15 @@ RSpec.describe QuickActions::TargetService do
shared_examples 'find target' do
it 'returns the target' do
- found_target = service.execute(type, target_id)
+ found_target = service.execute(type, target_iid)
expect(found_target).to eq(target)
end
end
- shared_examples 'build target' do |type_id:|
+ shared_examples 'build target' do |type_iid:|
it 'builds a new target' do
- target = service.execute(type, type_id)
+ target = service.execute(type, type_iid)
expect(target.project).to eq(project)
expect(target).to be_new_record
@@ -39,36 +39,44 @@ RSpec.describe QuickActions::TargetService do
context 'for issue' do
let(:target) { create(:issue, project: project) }
- let(:target_id) { target.iid }
+ let(:target_iid) { target.iid }
let(:type) { 'Issue' }
it_behaves_like 'find target'
- it_behaves_like 'build target', type_id: nil
- it_behaves_like 'build target', type_id: -1
+ it_behaves_like 'build target', type_iid: nil
+ it_behaves_like 'build target', type_iid: -1
+ end
+
+ context 'for work item' do
+ let(:target) { create(:work_item, :task, project: project) }
+ let(:target_iid) { target.iid }
+ let(:type) { 'WorkItem' }
+
+ it_behaves_like 'find target'
end
context 'for merge request' do
let(:target) { create(:merge_request, source_project: project) }
- let(:target_id) { target.iid }
+ let(:target_iid) { target.iid }
let(:type) { 'MergeRequest' }
it_behaves_like 'find target'
- it_behaves_like 'build target', type_id: nil
- it_behaves_like 'build target', type_id: -1
+ it_behaves_like 'build target', type_iid: nil
+ it_behaves_like 'build target', type_iid: -1
end
context 'for commit' do
let(:project) { create(:project, :repository) }
let(:target) { project.commit.parent }
- let(:target_id) { target.sha }
+ let(:target_iid) { target.sha }
let(:type) { 'Commit' }
it_behaves_like 'find target'
- it_behaves_like 'no target', type_id: 'invalid_sha'
+ it_behaves_like 'no target', type_iid: 'invalid_sha'
- context 'with nil target_id' do
+ context 'with nil target_iid' do
let(:target) { project.commit }
- let(:target_id) { nil }
+ let(:target_iid) { nil }
it_behaves_like 'find target'
end
@@ -77,7 +85,7 @@ RSpec.describe QuickActions::TargetService do
context 'for unknown type' do
let(:type) { 'unknown' }
- it_behaves_like 'no target', type_id: :unused
+ it_behaves_like 'no target', type_iid: :unused
end
end
end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 5f49eed3e77..9768ceb12e8 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::CreateService do
+RSpec.describe Releases::CreateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
@@ -132,6 +132,15 @@ RSpec.describe Releases::CreateService do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
+
+ it 'raises an error saying the milestone id is inexistent' do
+ inexistent_milestone_id = non_existing_record_id
+ service = described_class.new(project, user, params.merge!({ milestone_ids: [inexistent_milestone_id] }))
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
+ end
end
context 'when existing milestone is passed in' do
@@ -140,15 +149,27 @@ RSpec.describe Releases::CreateService do
let(:params_with_milestone) { params.merge!({ milestones: [title] }) }
let(:service) { described_class.new(milestone.project, user, params_with_milestone) }
- it 'creates a release and ties this milestone to it' do
- result = service.execute
+ shared_examples 'creates release' do
+ it 'creates a release and ties this milestone to it' do
+ result = service.execute
- expect(project.releases.count).to eq(1)
- expect(result[:status]).to eq(:success)
+ expect(project.releases.count).to eq(1)
+ expect(result[:status]).to eq(:success)
+
+ release = project.releases.last
+
+ expect(release.milestones).to match_array([milestone])
+ end
+ end
- release = project.releases.last
+ context 'by title' do
+ it_behaves_like 'creates release'
+ end
+
+ context 'by ids' do
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [milestone.id] }) }
- expect(release.milestones).to match_array([milestone])
+ it_behaves_like 'creates release'
end
context 'when another release was previously created with that same milestone linked' do
@@ -164,18 +185,31 @@ RSpec.describe Releases::CreateService do
end
end
- context 'when multiple existing milestone titles are passed in' do
+ context 'when multiple existing milestones are passed in' do
let(:title_1) { 'v1.0' }
let(:title_2) { 'v1.0-rc' }
let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }
- let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
- it 'creates a release and ties it to these milestones' do
- described_class.new(project, user, params_with_milestones).execute
- release = project.releases.last
+ shared_examples 'creates multiple releases' do
+ it 'creates a release and ties it to these milestones' do
+ described_class.new(project, user, params_with_milestones).execute
+ release = project.releases.last
+
+ expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ end
+ end
+
+ context 'by title' do
+ let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }
+
+ it_behaves_like 'creates multiple releases'
+ end
+
+ context 'by ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone_1.id, milestone_2.id] }) }
- expect(release.milestones.map(&:title)).to include(title_1, title_2)
+ it_behaves_like 'creates multiple releases'
end
end
@@ -198,6 +232,17 @@ RSpec.describe Releases::CreateService do
service.execute
end.not_to change(Release, :count)
end
+
+ context 'with milestones as ids' do
+ let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone.id, non_existing_record_id] }) }
+
+ it 'raises an error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
+ end
+ end
end
context 'no milestone association behavior' do
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 7461470a844..6bddea48251 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Releases::UpdateService do
+RSpec.describe Releases::UpdateService, feature_category: :continuous_integration do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:new_name) { 'A new name' }
@@ -60,18 +60,22 @@ RSpec.describe Releases::UpdateService do
release.milestones << milestone
end
- context 'a different milestone' do
- let(:new_title) { 'v2.0' }
-
+ shared_examples 'updates milestones' do
it 'updates the related milestone accordingly' do
- result = service.execute
release.reload
+ result = service.execute
expect(release.milestones.first.title).to eq(new_title)
expect(result[:milestones_updated]).to be_truthy
end
end
+ context 'a different milestone' do
+ let(:new_title) { 'v2.0' }
+
+ it_behaves_like 'updates milestones'
+ end
+
context 'an identical milestone' do
let(:new_title) { 'v1.0' }
@@ -79,11 +83,17 @@ RSpec.describe Releases::UpdateService do
expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ context 'by ids' do
+ let(:new_title) { 'v2.0' }
+ let(:params_with_milestone) { params.merge!({ milestone_ids: [new_milestone.id] }) }
+
+ it_behaves_like 'updates milestones'
+ end
end
context "when an 'empty' milestone is passed in" do
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
before do
release.milestones << milestone
@@ -91,12 +101,26 @@ RSpec.describe Releases::UpdateService do
service.params = params_with_empty_milestone
end
- it 'removes the old milestone and does not associate any new milestone' do
- result = service.execute
- release.reload
+ shared_examples 'removes milestones' do
+ it 'removes the old milestone and does not associate any new milestone' do
+ result = service.execute
+ release.reload
+
+ expect(release.milestones).not_to be_present
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
- expect(release.milestones).not_to be_present
- expect(result[:milestones_updated]).to be_truthy
+ context 'by title' do
+ let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) }
+
+ it_behaves_like 'removes milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_empty_milestone) { params.merge!({ milestone_ids: [] }) }
+
+ it_behaves_like 'removes milestones'
end
end
@@ -104,22 +128,35 @@ RSpec.describe Releases::UpdateService do
let(:new_title_1) { 'v2.0' }
let(:new_title_2) { 'v2.0-rc' }
let(:milestone) { create(:milestone, project: project, title: 'v1.0') }
- let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
let(:service) { described_class.new(project, user, params_with_milestones) }
+ let!(:new_milestone_1) { create(:milestone, project: project, title: new_title_1) }
+ let!(:new_milestone_2) { create(:milestone, project: project, title: new_title_2) }
before do
- create(:milestone, project: project, title: new_title_1)
- create(:milestone, project: project, title: new_title_2)
release.milestones << milestone
end
- it 'removes the old milestone and update the release with the new ones' do
- result = service.execute
- release.reload
+ shared_examples 'updates multiple milestones' do
+ it 'removes the old milestone and update the release with the new ones' do
+ result = service.execute
+ release.reload
+
+ milestone_titles = release.milestones.map(&:title)
+ expect(milestone_titles).to match_array([new_title_1, new_title_2])
+ expect(result[:milestones_updated]).to be_truthy
+ end
+ end
+
+ context 'by title' do
+ let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) }
+
+ it_behaves_like 'updates multiple milestones'
+ end
+
+ context 'by id' do
+ let(:params_with_milestones) { params.merge!({ milestone_ids: [new_milestone_1.id, new_milestone_2.id] }) }
- milestone_titles = release.milestones.map(&:title)
- expect(milestone_titles).to match_array([new_title_1, new_title_2])
- expect(result[:milestones_updated]).to be_truthy
+ it_behaves_like 'updates multiple milestones'
end
end
end
diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb
index 9b0ca54a394..d94b49de9d7 100644
--- a/spec/services/resource_events/change_labels_service_spec.rb
+++ b/spec/services/resource_events/change_labels_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe ResourceEvents::ChangeLabelsService do
+# feature category is shared among plan(issues, epics), monitor(incidents), create(merge request) stages
+RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :shared do
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
@@ -86,12 +87,30 @@ RSpec.describe ResourceEvents::ChangeLabelsService do
let(:added) { [labels[0]] }
let(:removed) { [labels[1]] }
+ it_behaves_like 'creating timeline events'
+
it 'creates all label events in a single query' do
expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original
expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2)
end
- it_behaves_like 'creating timeline events'
+ context 'when resource is a work item' do
+ it 'triggers note created subscription' do
+ expect(GraphqlTriggers).to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
+
+ context 'when resource is an MR' do
+ let(:resource) { create(:merge_request, source_project: project) }
+
+ it 'does not trigger note created subscription' do
+ expect(GraphqlTriggers).not_to receive(:work_item_note_created)
+
+ change_labels
+ end
+ end
end
describe 'usage data' do
diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb
index 1e6dc367146..e80fe1a42fa 100644
--- a/spec/services/security/ci_configuration/sast_create_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_category: :sast do
+RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow,
+ feature_category: :static_application_security_testing do
subject(:result) { described_class.new(project, user, params).execute }
let(:branch_name) { 'set-sast-config-1' }
@@ -24,7 +25,45 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
include_examples 'services security ci configuration create service'
- context "when committing to the default branch", :aggregate_failures do
+ RSpec.shared_examples_for 'commits directly to the default branch' do
+ it 'commits directly to the default branch' do
+ expect(project).to receive(:default_branch).twice.and_return('master')
+
+ expect(result.status).to eq(:success)
+ expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
+ expect(result.payload[:branch]).to eq('master')
+ end
+ end
+
+ context 'when the repository is empty' do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ context 'when initialize_with_sast is false' do
+ before do
+ project.add_developer(user)
+ end
+
+ let(:params) { { initialize_with_sast: false } }
+
+ it 'raises an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError)
+ end
+ end
+
+ context 'when initialize_with_sast is true' do
+ let(:params) { { initialize_with_sast: true } }
+
+ subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'commits directly to the default branch'
+ end
+ end
+
+ context 'when committing to the default branch', :aggregate_failures do
subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute }
let(:params) { {} }
@@ -33,17 +72,13 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_
project.add_developer(user)
end
- it "doesn't try to remove that branch on raised exceptions" do
+ it 'does not try to remove that branch on raised exceptions' do
expect(Files::MultiService).to receive(:new).and_raise(StandardError, '_exception_')
expect(project.repository).not_to receive(:rm_branch)
expect { result }.to raise_error(StandardError, '_exception_')
end
- it "commits directly to the default branch" do
- expect(result.status).to eq(:success)
- expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/)
- expect(result.payload[:branch]).to eq('master')
- end
+ it_behaves_like 'commits directly to the default branch'
end
end
diff --git a/spec/services/serverless/associate_domain_service_spec.rb b/spec/services/serverless/associate_domain_service_spec.rb
index 3b5231989bc..2f45806589e 100644
--- a/spec/services/serverless/associate_domain_service_spec.rb
+++ b/spec/services/serverless/associate_domain_service_spec.rb
@@ -3,13 +3,24 @@
require 'spec_helper'
RSpec.describe Serverless::AssociateDomainService do
- subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) }
+ let_it_be(:sdc_pages_domain) { create(:pages_domain, :instance_serverless) }
+ let_it_be(:sdc_cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
+ let_it_be(:sdc_knative) { create(:clusters_applications_knative, cluster: sdc_cluster) }
+ let_it_be(:sdc_creator) { create(:user) }
+
+ let(:sdc) do
+ create(:serverless_domain_cluster,
+ knative: sdc_knative,
+ creator: sdc_creator,
+ pages_domain: sdc_pages_domain)
+ end
- let(:sdc) { create(:serverless_domain_cluster, pages_domain: create(:pages_domain, :instance_serverless)) }
let(:knative) { sdc.knative }
let(:creator) { sdc.creator }
let(:pages_domain_id) { sdc.pages_domain_id }
+ subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) }
+
context 'when the domain is unchanged' do
let(:creator) { create(:user) }
@@ -19,8 +30,8 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when domain is changed to nil' do
- let(:pages_domain_id) { nil }
- let(:creator) { create(:user) }
+ let_it_be(:creator) { create(:user) }
+ let_it_be(:pages_domain_id) { nil }
it 'removes the association between knative and the domain' do
expect { subject.execute }.to change { knative.reload.pages_domain }.from(sdc.pages_domain).to(nil)
@@ -32,11 +43,13 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when a new domain is associated' do
- let(:pages_domain_id) { create(:pages_domain, :instance_serverless).id }
- let(:creator) { create(:user) }
+ let_it_be(:creator) { create(:user) }
+ let_it_be(:pages_domain_id) { create(:pages_domain, :instance_serverless).id }
it 'creates an association with the domain' do
- expect { subject.execute }.to change { knative.pages_domain.id }.from(sdc.pages_domain.id).to(pages_domain_id)
+ expect { subject.execute }.to change { knative.reload.pages_domain.id }
+ .from(sdc.pages_domain.id)
+ .to(pages_domain_id)
end
it 'updates creator' do
@@ -45,7 +58,7 @@ RSpec.describe Serverless::AssociateDomainService do
end
context 'when knative is not authorized to use the pages domain' do
- let(:pages_domain_id) { create(:pages_domain).id }
+ let_it_be(:pages_domain_id) { create(:pages_domain).id }
before do
expect(knative).to receive(:available_domains).and_return(PagesDomain.none)
@@ -56,19 +69,23 @@ RSpec.describe Serverless::AssociateDomainService do
end
end
- context 'when knative hostname is nil' do
- let(:knative) { build(:clusters_applications_knative, hostname: nil) }
+ describe 'for new knative application' do
+ let_it_be(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) }
- it 'sets hostname to a placeholder value' do
- expect { subject.execute }.to change { knative.hostname }.to('example.com')
+ context 'when knative hostname is nil' do
+ let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: nil) }
+
+ it 'sets hostname to a placeholder value' do
+ expect { subject.execute }.to change { knative.hostname }.to('example.com')
+ end
end
- end
- context 'when knative hostname exists' do
- let(:knative) { build(:clusters_applications_knative, hostname: 'hostname.com') }
+ context 'when knative hostname exists' do
+ let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: 'hostname.com') }
- it 'does not change hostname' do
- expect { subject.execute }.not_to change { knative.hostname }
+ it 'does not change hostname' do
+ expect { subject.execute }.not_to change { knative.hostname }
+ end
end
end
end
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index b89c96129c2..dde93aa6b93 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -28,10 +28,6 @@ RSpec.describe Spam::SpamVerdictService do
extra_attributes
end
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
shared_examples 'execute spam verdict service' do
subject { service.execute }
@@ -119,9 +115,9 @@ RSpec.describe Spam::SpamVerdictService do
end
end
- context 'if allow_possible_spam flag is true' do
+ context 'if allow_possible_spam application setting is true' do
before do
- stub_feature_flags(allow_possible_spam: true)
+ stub_application_setting(allow_possible_spam: true)
end
context 'and a service returns a verdict that should be overridden' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a192fae27db..38b6943b12a 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SystemNoteService do
+RSpec.describe SystemNoteService, feature_category: :shared do
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb
index bf6be6d46e5..cfeff36cc0d 100644
--- a/spec/services/tasks_to_be_done/base_service_spec.rb
+++ b/spec/services/tasks_to_be_done/base_service_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe TasksToBeDone::BaseService do
subject(:service) do
TasksToBeDone::CreateCiTaskService.new(
- project: project,
+ container: project,
current_user: current_user,
assignee_ids: assignee_ids
)
@@ -35,7 +35,7 @@ RSpec.describe TasksToBeDone::BaseService do
expect(Issues::BuildService)
.to receive(:new)
- .with(project: project, current_user: current_user, params: params)
+ .with(container: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.to change(Issue, :count).by(1)
@@ -58,7 +58,7 @@ RSpec.describe TasksToBeDone::BaseService do
expect(Issues::UpdateService)
.to receive(:new)
- .with(project: project, current_user: current_user, params: params)
+ .with(container: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.not_to change(Issue, :count)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 596ca9495ff..f73eae70d3c 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1224,6 +1224,24 @@ RSpec.describe TodoService do
end
end
+ describe '#resolve_access_request_todos' do
+ let_it_be(:source) { create(:group, :public) }
+ let_it_be(:requester) { create(:group_member, :access_request, group: source, user: assignee) }
+
+ it 'marks the todos for request handler as done' do
+ request_handler_todo = create(:todo,
+ user: member,
+ state: :pending,
+ action: Todo::MEMBER_ACCESS_REQUESTED,
+ author: requester.user,
+ target: source)
+
+ service.resolve_access_request_todos(member, requester)
+
+ expect(request_handler_todo.reload).to be_done
+ end
+ end
+
describe '#restore_todo' do
let!(:todo) { create(:todo, :done, user: john_doe) }
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 9d5ed70e9ef..1ced2eda799 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::EntityLeaveService do
+RSpec.describe Todos::Destroy::EntityLeaveService, feature_category: :team_planning do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:user2, reload: true) { create(:user) }
let_it_be_with_refind(:group) { create(:group, :private) }
diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb
index 30d02cb7400..be470688084 100644
--- a/spec/services/todos/destroy/group_private_service_spec.rb
+++ b/spec/services/todos/destroy/group_private_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todos::Destroy::GroupPrivateService do
+RSpec.describe Todos::Destroy::GroupPrivateService, feature_category: :team_planning do
let(:group) { create(:group, :public) }
let(:project) { create(:project, group: group) }
let(:user) { create(:user) }
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index be4f205afb5..356675d55f2 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -2,33 +2,39 @@
require 'spec_helper'
-RSpec.describe UserProjectAccessChangedService do
+RSpec.describe UserProjectAccessChangedService, feature_category: :authentication_and_authorization do
describe '#execute' do
- it 'schedules the user IDs' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ it 'permits high-priority operation' do
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
described_class.new([1, 2]).execute
end
- it 'permits non-blocking operation' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
- .with([[1], [2]])
+ context 'for low priority operation' do
+ context 'when the feature flag `do_not_run_safety_net_auth_refresh_jobs` is disabled' do
+ before do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+ end
+
+ it 'permits low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in).with(
+ described_class::DELAY,
+ [[1], [2]],
+ { batch_delay: 30.seconds, batch_size: 100 }
+ )
+ )
+
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
+ end
+ end
- described_class.new([1, 2]).execute(blocking: false)
- end
+ it 'does not perform low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).not_to receive(:bulk_perform_in)
- it 'permits low-priority operation' do
- expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
- receive(:bulk_perform_in).with(
- described_class::DELAY,
- [[1], [2]],
- { batch_delay: 30.seconds, batch_size: 100 }
- )
- )
-
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::LOW_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
+ end
end
it 'permits medium-priority operation' do
@@ -40,14 +46,12 @@ RSpec.describe UserProjectAccessChangedService do
)
)
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::MEDIUM_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::MEDIUM_PRIORITY)
end
it 'sets the current caller_id as related_class in the context of all the enqueued jobs' do
Gitlab::ApplicationContext.with_context(caller_id: 'Foo') do
- described_class.new([1, 2]).execute(blocking: false,
- priority: described_class::LOW_PRIORITY)
+ described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY)
end
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker.jobs).to all(
@@ -60,7 +64,7 @@ RSpec.describe UserProjectAccessChangedService do
let(:service) { UserProjectAccessChangedService.new([1, 2]) }
before do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
.and_return(10)
end
@@ -79,7 +83,7 @@ RSpec.describe UserProjectAccessChangedService do
service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5])
- allow(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ allow(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
.with([[1], [2], [3], [4], [5]])
.and_return(10)
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 47a4b943d83..6c0d93f568a 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -7,9 +7,21 @@ RSpec.describe Users::ActivityService do
let(:user) { create(:user, last_activity_on: last_activity_on) }
- subject { described_class.new(user) }
+ subject { described_class.new(author: user) }
describe '#execute', :clean_gitlab_redis_shared_state do
+ shared_examples 'does not update last_activity_on' do
+ it 'does not update user attribute' do
+ expect { subject.execute }.not_to change(user, :last_activity_on)
+ end
+
+ it 'does not track Snowplow event' do
+ subject.execute
+
+ expect_no_snowplow_event
+ end
+ end
+
context 'when last activity is nil' do
let(:last_activity_on) { nil }
@@ -41,13 +53,29 @@ RSpec.describe Users::ActivityService do
subject.execute
end
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:record_activity) { described_class.new(author: user, namespace: namespace, project: project).execute }
+
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase3 }
+ let(:category) { described_class.name }
+ let(:action) { 'perform_action' }
+ let(:label) { 'redis_hll_counters.manage.unique_active_users_monthly' }
+ let(:namespace) { build(:group) }
+ let(:project) { build(:project) }
+ let(:context) do
+ payload = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'unique_active_user').to_context
+ [Gitlab::Json.dump(payload)]
+ end
+ end
end
context 'when a bad object is passed' do
let(:fake_object) { double(username: 'hello') }
it 'does not record activity' do
- service = described_class.new(fake_object)
+ service = described_class.new(author: fake_object)
expect(service).not_to receive(:record_activity)
@@ -58,9 +86,7 @@ RSpec.describe Users::ActivityService do
context 'when last activity is today' do
let(:last_activity_on) { Date.today }
- it 'does not update last_activity_on' do
- expect { subject.execute }.not_to change(user, :last_activity_on)
- end
+ it_behaves_like 'does not update last_activity_on'
it 'does not try to obtain ExclusiveLease' do
expect(Gitlab::ExclusiveLease).not_to receive(:new).with("activity_service:#{user.id}", anything)
@@ -76,19 +102,17 @@ RSpec.describe Users::ActivityService do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
- it 'does not update last_activity_on' do
- expect { subject.execute }.not_to change(user, :last_activity_on)
- end
+ it_behaves_like 'does not update last_activity_on'
end
context 'when a lease could not be obtained' do
let(:last_activity_on) { nil }
- it 'does not update last_activity_on' do
+ before do
stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i)
-
- expect { subject.execute }.not_to change(user, :last_activity_on)
end
+
+ it_behaves_like 'does not update last_activity_on'
end
end
@@ -104,7 +128,7 @@ RSpec.describe Users::ActivityService do
end
let(:service) do
- service = described_class.new(user)
+ service = described_class.new(author: user)
::Gitlab::Database::LoadBalancing::Session.clear_session
@@ -123,7 +147,7 @@ RSpec.describe Users::ActivityService do
end
context 'database load balancing is not configured' do
- let(:service) { described_class.new(user) }
+ let(:service) { described_class.new(author: user) }
it 'updates user without error' do
service.execute
diff --git a/spec/services/users/assigned_issues_count_service_spec.rb b/spec/services/users/assigned_issues_count_service_spec.rb
index afa6a0af3dd..2062f68b24b 100644
--- a/spec/services/users/assigned_issues_count_service_spec.rb
+++ b/spec/services/users/assigned_issues_count_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::AssignedIssuesCountService, :use_clean_rails_memory_store_caching,
- feature_category: :project_management do
+ feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:max_limit) { 10 }
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 4b925a058e7..5736bf885be 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do
+RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state, feature_category: :integrations do
include StubRequests
let(:ellipsis) { '…' }
@@ -358,6 +358,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
{
trigger: 'push_hooks',
url: project_hook.url,
+ interpolated_url: project_hook.interpolated_url,
request_headers: headers,
request_data: data,
response_body: 'Success',
diff --git a/spec/services/work_items/build_service_spec.rb b/spec/services/work_items/build_service_spec.rb
index 6b2e2d8819e..405b4414fc2 100644
--- a/spec/services/work_items/build_service_spec.rb
+++ b/spec/services/work_items/build_service_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe WorkItems::BuildService do
end
describe '#execute' do
- subject { described_class.new(project: project, current_user: user, params: {}).execute }
+ subject { described_class.new(container: project, current_user: user, params: {}).execute }
it { is_expected.to be_a(::WorkItem) }
end
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index 049c90f20b0..1b134c308f2 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe WorkItems::CreateService do
describe '#execute' do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -118,7 +118,7 @@ RSpec.describe WorkItems::CreateService do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -188,7 +188,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
diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb
index 6cca5018852..69ae881a12f 100644
--- a/spec/services/work_items/delete_service_spec.rb
+++ b/spec/services/work_items/delete_service_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe WorkItems::DeleteService do
end
describe '#execute' do
- subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) }
+ subject(:result) { described_class.new(container: project, current_user: user).execute(work_item) }
context 'when user can delete the work item' do
it { is_expected.to be_success }
diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb
new file mode 100644
index 00000000000..0718d3b686a
--- /dev/null
+++ b/spec/services/work_items/export_csv_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :team_planning do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:work_item_1) { create(:work_item, project: project) }
+ let_it_be(:work_item_2) { create(:work_item, :incident, project: project) }
+
+ subject { described_class.new(WorkItem.all, project) }
+
+ def csv
+ CSV.parse(subject.csv_data, headers: true)
+ end
+
+ context 'when import_export_work_items_csv flag is not enabled' do
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it 'renders an error' do
+ expect { subject.csv_data }.to raise_error(described_class::NotAvailableError)
+ end
+ end
+
+ it 'renders csv to string' do
+ expect(subject.csv_data).to be_a String
+ end
+
+ describe '#email' do
+ # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082
+ xit 'emails csv' do
+ expect { subject.email(user) }.o change { ActionMailer::Base.deliveries.count }.from(0).to(1)
+ end
+ end
+
+ it 'returns two work items' do
+ expect(csv.count).to eq(2)
+ end
+
+ specify 'iid' do
+ expect(csv[0]['Id']).to eq work_item_1.iid.to_s
+ end
+
+ specify 'title' do
+ expect(csv[0]['Title']).to eq work_item_1.title
+ end
+
+ specify 'type' do
+ expect(csv[0]['Type']).to eq('Issue')
+ expect(csv[1]['Type']).to eq('Incident')
+ end
+
+ specify 'author name' do
+ expect(csv[0]['Author']).to eq(work_item_1.author_name)
+ end
+
+ specify 'author username' do
+ expect(csv[0]['Author Username']).to eq(work_item_1.author.username)
+ end
+
+ specify 'created_at' do
+ expect(csv[0]['Created At (UTC)']).to eq(work_item_1.created_at.to_s(:csv))
+ end
+
+ it 'preloads fields to avoid N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { subject.csv_data }
+
+ create(:work_item, :task, project: project)
+
+ expect { subject.csv_data }.not_to exceed_query_limit(control)
+ end
+
+ it_behaves_like 'a service that returns invalid fields from selection'
+end
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 87665bcad2c..435995c6570 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe WorkItems::UpdateService do
describe '#execute' do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -146,7 +146,7 @@ RSpec.describe WorkItems::UpdateService do
let(:service) do
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
@@ -362,7 +362,7 @@ RSpec.describe WorkItems::UpdateService do
def update_issuable(update_params)
described_class.new(
- project: project,
+ container: project,
current_user: current_user,
params: update_params,
spam_params: spam_params,
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f33c6e64b0c..4e8f990fc10 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -44,7 +44,7 @@ rspec_profiling_is_configured =
ENV['RSPEC_PROFILING']
branch_can_be_profiled =
(ENV['CI_COMMIT_REF_NAME'] == 'master' ||
- ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/)
+ ENV['CI_COMMIT_REF_NAME']&.include?('rspec-profile'))
if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled)
require 'rspec_profiling/rspec'
@@ -105,7 +105,7 @@ RSpec.configure do |config|
location = metadata[:location]
metadata[:level] = quality_level.level_for(location)
- metadata[:api] = true if location =~ %r{/spec/requests/api/}
+ metadata[:api] = true if location.include?('/spec/requests/api/')
# Do not overwrite migration if it's already set
unless metadata.key?(:migration)
@@ -294,7 +294,6 @@ RSpec.configure do |config|
stub_feature_flags(block_issue_repositioning: false)
# These are ops feature flags that are disabled by default
- stub_feature_flags(disable_anonymous_search: false)
stub_feature_flags(disable_anonymous_project_search: false)
# Specs should not get a CAPTCHA challenge by default, this makes the sign-in flow simpler in
@@ -352,6 +351,101 @@ RSpec.configure do |config|
end
end
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/42692
+ # The ongoing implementation of Admin Mode for API is behind the :admin_mode_for_api feature flag.
+ # All API specs will be adapted continuously. The following list contains the specs that have not yet been adapted.
+ # The feature flag is disabled for these specs as long as they are not yet adapted.
+ admin_mode_for_api_feature_flag_paths = %w[
+ ./spec/frontend/fixtures/api_deploy_keys.rb
+ ./spec/requests/api/admin/batched_background_migrations_spec.rb
+ ./spec/requests/api/admin/ci/variables_spec.rb
+ ./spec/requests/api/admin/instance_clusters_spec.rb
+ ./spec/requests/api/admin/plan_limits_spec.rb
+ ./spec/requests/api/admin/sidekiq_spec.rb
+ ./spec/requests/api/broadcast_messages_spec.rb
+ ./spec/requests/api/ci/pipelines_spec.rb
+ ./spec/requests/api/ci/runners_reset_registration_token_spec.rb
+ ./spec/requests/api/ci/runners_spec.rb
+ ./spec/requests/api/deploy_keys_spec.rb
+ ./spec/requests/api/deploy_tokens_spec.rb
+ ./spec/requests/api/freeze_periods_spec.rb
+ ./spec/requests/api/graphql/user/starred_projects_query_spec.rb
+ ./spec/requests/api/groups_spec.rb
+ ./spec/requests/api/issues/get_group_issues_spec.rb
+ ./spec/requests/api/issues/get_project_issues_spec.rb
+ ./spec/requests/api/issues/issues_spec.rb
+ ./spec/requests/api/issues/post_projects_issues_spec.rb
+ ./spec/requests/api/issues/put_projects_issues_spec.rb
+ ./spec/requests/api/keys_spec.rb
+ ./spec/requests/api/merge_requests_spec.rb
+ ./spec/requests/api/namespaces_spec.rb
+ ./spec/requests/api/notes_spec.rb
+ ./spec/requests/api/pages/internal_access_spec.rb
+ ./spec/requests/api/pages/pages_spec.rb
+ ./spec/requests/api/pages/private_access_spec.rb
+ ./spec/requests/api/pages/public_access_spec.rb
+ ./spec/requests/api/pages_domains_spec.rb
+ ./spec/requests/api/personal_access_tokens/self_information_spec.rb
+ ./spec/requests/api/personal_access_tokens_spec.rb
+ ./spec/requests/api/project_export_spec.rb
+ ./spec/requests/api/project_repository_storage_moves_spec.rb
+ ./spec/requests/api/project_snapshots_spec.rb
+ ./spec/requests/api/project_snippets_spec.rb
+ ./spec/requests/api/projects_spec.rb
+ ./spec/requests/api/releases_spec.rb
+ ./spec/requests/api/sidekiq_metrics_spec.rb
+ ./spec/requests/api/snippet_repository_storage_moves_spec.rb
+ ./spec/requests/api/snippets_spec.rb
+ ./spec/requests/api/statistics_spec.rb
+ ./spec/requests/api/system_hooks_spec.rb
+ ./spec/requests/api/topics_spec.rb
+ ./spec/requests/api/usage_data_non_sql_metrics_spec.rb
+ ./spec/requests/api/usage_data_queries_spec.rb
+ ./spec/requests/api/users_spec.rb
+ ./spec/requests/api/v3/github_spec.rb
+ ./spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/notes_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/pipelines/visibility_table_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+ ./spec/support/shared_examples/requests/api/status_shared_examples.rb
+ ./spec/support/shared_examples/requests/clusters/certificate_based_clusters_feature_flag_shared_examples.rb
+ ./spec/support/shared_examples/requests/snippet_shared_examples.rb
+ ./ee/spec/requests/api/audit_events_spec.rb
+ ./ee/spec/requests/api/ci/minutes_spec.rb
+ ./ee/spec/requests/api/elasticsearch_indexed_namespaces_spec.rb
+ ./ee/spec/requests/api/epics_spec.rb
+ ./ee/spec/requests/api/geo_nodes_spec.rb
+ ./ee/spec/requests/api/geo_replication_spec.rb
+ ./ee/spec/requests/api/geo_spec.rb
+ ./ee/spec/requests/api/group_push_rule_spec.rb
+ ./ee/spec/requests/api/group_repository_storage_moves_spec.rb
+ ./ee/spec/requests/api/groups_spec.rb
+ ./ee/spec/requests/api/internal/upcoming_reconciliations_spec.rb
+ ./ee/spec/requests/api/invitations_spec.rb
+ ./ee/spec/requests/api/license_spec.rb
+ ./ee/spec/requests/api/merge_request_approvals_spec.rb
+ ./ee/spec/requests/api/namespaces_spec.rb
+ ./ee/spec/requests/api/notes_spec.rb
+ ./ee/spec/requests/api/project_aliases_spec.rb
+ ./ee/spec/requests/api/project_approval_rules_spec.rb
+ ./ee/spec/requests/api/project_approval_settings_spec.rb
+ ./ee/spec/requests/api/project_approvals_spec.rb
+ ./ee/spec/requests/api/projects_spec.rb
+ ./ee/spec/requests/api/settings_spec.rb
+ ./ee/spec/requests/api/users_spec.rb
+ ./ee/spec/requests/api/vulnerabilities_spec.rb
+ ./ee/spec/requests/api/vulnerability_exports_spec.rb
+ ./ee/spec/requests/api/vulnerability_findings_spec.rb
+ ./ee/spec/requests/api/vulnerability_issue_links_spec.rb
+ ./ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb
+ ]
+
+ if example.metadata[:file_path].start_with?(*admin_mode_for_api_feature_flag_paths)
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
# Make sure specs test by default admin mode setting on, unless forced to the opposite
stub_application_setting(admin_mode: true) unless example.metadata[:do_not_mock_admin_mode_setting]
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
index 759e8316cc5..cd0cbe733d1 100644
--- a/spec/support/database/prevent_cross_database_modification.rb
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -28,8 +28,6 @@ RSpec.configure do |config|
config.after do |example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress_in_rspec = true
- [::ApplicationRecord, ::Ci::ApplicationRecord].each do |base_class|
- base_class.gitlab_transactions_stack.clear if base_class.respond_to?(:gitlab_transactions_stack)
- end
+ ::ApplicationRecord.gitlab_transactions_stack.clear
end
end
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
index 42c69a26788..8e08824c464 100644
--- a/spec/support/database/prevent_cross_joins.rb
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -23,6 +23,7 @@ module Database
ALLOW_THREAD_KEY = :allow_cross_joins_across_databases
ALLOW_ANNOTATE_KEY = ALLOW_THREAD_KEY.to_s.freeze
+ IGNORED_SCHEMAS = %i[gitlab_shared gitlab_internal].freeze
def self.validate_cross_joins!(sql)
return if Thread.current[ALLOW_THREAD_KEY] || sql.include?(ALLOW_ANNOTATE_KEY)
@@ -40,8 +41,9 @@ module Database
end
schemas = ::Gitlab::Database::GitlabSchema.table_schemas(tables)
+ schemas.subtract(IGNORED_SCHEMAS)
- if schemas.include?(:gitlab_ci) && schemas.include?(:gitlab_main)
+ if schemas.many?
Thread.current[:has_cross_join_exception] = true
raise CrossJoinAcrossUnsupportedTablesError,
"Unsupported cross-join across '#{tables.join(", ")}' querying '#{schemas.to_a.join(", ")}' discovered " \
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 588fe466a42..b9a99eff413 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -12,7 +12,7 @@ module DbCleaner
end
def deletion_except_tables
- %w[work_item_types work_item_hierarchy_restrictions]
+ %w[work_item_types work_item_hierarchy_restrictions work_item_widget_definitions]
end
def setup_database_cleaner
diff --git a/spec/support/graphql/fake_tracer.rb b/spec/support/graphql/fake_tracer.rb
index c2fb7ed12d8..58688c9abd0 100644
--- a/spec/support/graphql/fake_tracer.rb
+++ b/spec/support/graphql/fake_tracer.rb
@@ -6,8 +6,8 @@ module Graphql
@trace_callback = trace_callback
end
- def trace(*args)
- @trace_callback.call(*args)
+ def trace(...)
+ @trace_callback.call(...)
yield
end
diff --git a/spec/support/graphql/resolver_factories.rb b/spec/support/graphql/resolver_factories.rb
index 3c5aad34e8b..76df4b58943 100644
--- a/spec/support/graphql/resolver_factories.rb
+++ b/spec/support/graphql/resolver_factories.rb
@@ -27,8 +27,8 @@ module Graphql
Class.new(Resolvers::BaseResolver) do
include ::Gitlab::Graphql::Authorize::AuthorizeResource
- def resolve(**args)
- authorized_find!(**args)
+ def resolve(...)
+ authorized_find!(...)
end
define_method :find_object do |**_args|
diff --git a/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb b/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb
new file mode 100644
index 00000000000..5467564a79e
--- /dev/null
+++ b/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+# A stub implementation of ActionCable.
+# Any methods to support the mock backend have `mock` in the name.
+module Graphql
+ module Subscriptions
+ module ActionCable
+ class MockActionCable
+ class MockChannel
+ def initialize
+ @mock_broadcasted_messages = []
+ end
+
+ attr_reader :mock_broadcasted_messages
+
+ def stream_from(stream_name, coder: nil, &block)
+ # Rails uses `coder`, we don't
+ block ||= ->(msg) { @mock_broadcasted_messages << msg }
+ MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block)
+ end
+ end
+
+ class MockStream
+ def initialize
+ @mock_channels = {}
+ end
+
+ def add_mock_channel(channel, handler)
+ @mock_channels[channel] = handler
+ end
+
+ def mock_broadcast(message)
+ @mock_channels.each do |channel, handler|
+ handler && handler.call(message)
+ end
+ end
+ end
+
+ class << self
+ def clear_mocks
+ @mock_streams = {}
+ end
+
+ def server
+ self
+ end
+
+ def broadcast(stream_name, message)
+ stream = @mock_streams[stream_name]
+ stream && stream.mock_broadcast(message)
+ end
+
+ def mock_stream_for(stream_name)
+ @mock_streams[stream_name] ||= MockStream.new
+ end
+
+ def get_mock_channel
+ MockChannel.new
+ end
+
+ def mock_stream_names
+ @mock_streams.keys
+ end
+ end
+ end
+
+ class MockSchema < GraphQL::Schema
+ class << self
+ def find_by_gid(gid)
+ return unless gid
+
+ if gid.model_class < ApplicationRecord
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
+ elsif gid.model_class.respond_to?(:lazy_find)
+ gid.model_class.lazy_find(gid.model_id)
+ else
+ gid.find
+ end
+ end
+
+ def id_from_object(object, _type = nil, _ctx = nil)
+ unless object.respond_to?(:to_global_id)
+ # This is an error in our schema and needs to be solved. So raise a
+ # more meaningful error message
+ raise "#{object} does not implement `to_global_id`. " \
+ "Include `GlobalID::Identification` into `#{object.class}"
+ end
+
+ object.to_global_id
+ end
+ end
+
+ query(::Types::QueryType)
+ subscription(::Types::SubscriptionType)
+
+ use GraphQL::Subscriptions::ActionCableSubscriptions, action_cable: MockActionCable, action_cable_coder: JSON
+ end
+ end
+ end
+end
diff --git a/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb b/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb
new file mode 100644
index 00000000000..cd5d78cc78b
--- /dev/null
+++ b/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# A stub implementation of ActionCable.
+# Any methods to support the mock backend have `mock` in the name.
+module Graphql
+ module Subscriptions
+ module ActionCable
+ class MockGitlabSchema < GraphQL::Schema
+ class << self
+ def find_by_gid(gid)
+ return unless gid
+
+ if gid.model_class < ApplicationRecord
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
+ elsif gid.model_class.respond_to?(:lazy_find)
+ gid.model_class.lazy_find(gid.model_id)
+ else
+ gid.find
+ end
+ end
+
+ def id_from_object(object, _type = nil, _ctx = nil)
+ unless object.respond_to?(:to_global_id)
+ # This is an error in our schema and needs to be solved. So raise a
+ # more meaningful error message
+ raise "#{object} does not implement `to_global_id`. " \
+ "Include `GlobalID::Identification` into `#{object.class}"
+ end
+
+ object.to_global_id
+ end
+ end
+
+ query(::Types::QueryType)
+ subscription(::Types::SubscriptionType)
+
+ use GraphQL::Subscriptions::ActionCableSubscriptions, action_cable: MockActionCable, action_cable_coder: JSON
+ end
+ end
+ end
+end
diff --git a/spec/support/graphql/subscriptions/notes/helper.rb b/spec/support/graphql/subscriptions/notes/helper.rb
new file mode 100644
index 00000000000..9a552f9879e
--- /dev/null
+++ b/spec/support/graphql/subscriptions/notes/helper.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Graphql
+ module Subscriptions
+ module Notes
+ module Helper
+ def subscription_response
+ subscription_channel = subscribe
+ yield
+ subscription_channel.mock_broadcasted_messages.first
+ end
+
+ def notes_subscription(name, noteable, current_user)
+ mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel
+
+ query = case name
+ when 'workItemNoteDeleted'
+ note_deleted_subscription_query(name, noteable)
+ when 'workItemNoteUpdated'
+ note_updated_subscription_query(name, noteable)
+ when 'workItemNoteCreated'
+ note_created_subscription_query(name, noteable)
+ else
+ raise "Subscription query unknown: #{name}"
+ end
+
+ GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel })
+
+ mock_channel
+ end
+
+ def note_subscription(name, noteable, current_user)
+ mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel
+
+ query = <<~SUBSCRIPTION
+ subscription {
+ #{name}(noteableId: \"#{noteable.to_gid}\") {
+ id
+ body
+ }
+ }
+ SUBSCRIPTION
+
+ GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel })
+
+ mock_channel
+ end
+
+ private
+
+ def note_deleted_subscription_query(name, noteable)
+ <<~SUBSCRIPTION
+ subscription {
+ #{name}(noteableId: \"#{noteable.to_gid}\") {
+ id
+ discussionId
+ lastDiscussionNote
+ }
+ }
+ SUBSCRIPTION
+ end
+
+ def note_created_subscription_query(name, noteable)
+ <<~SUBSCRIPTION
+ subscription {
+ #{name}(noteableId: \"#{noteable.to_gid}\") {
+ id
+ discussion {
+ id
+ notes {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ }
+ SUBSCRIPTION
+ end
+
+ def note_updated_subscription_query(name, noteable)
+ <<~SUBSCRIPTION
+ subscription {
+ #{name}(noteableId: \"#{noteable.to_gid}\") {
+ id
+ body
+ }
+ }
+ SUBSCRIPTION
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/ci/job_token_scope_helpers.rb b/spec/support/helpers/ci/job_token_scope_helpers.rb
new file mode 100644
index 00000000000..1d71356917e
--- /dev/null
+++ b/spec/support/helpers/ci/job_token_scope_helpers.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Ci
+ module JobTokenScopeHelpers
+ def create_project_in_allowlist(root_project, direction:, target_project: nil)
+ included_project = target_project || create(:project,
+ ci_outbound_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ )
+ create(
+ :ci_job_token_project_scope_link,
+ source_project: root_project,
+ target_project: included_project,
+ direction: direction
+ )
+
+ included_project
+ end
+
+ def create_project_in_both_allowlists(root_project)
+ create_project_in_allowlist(root_project, direction: :outbound).tap do |new_project|
+ create_project_in_allowlist(root_project, target_project: new_project, direction: :inbound)
+ end
+ end
+
+ def create_inbound_accessible_project(project)
+ create(:project).tap do |accessible_project|
+ add_inbound_accessible_linkage(project, accessible_project)
+ end
+ end
+
+ def create_inbound_and_outbound_accessible_project(project)
+ create(:project).tap do |accessible_project|
+ make_project_fully_accessible(project, accessible_project)
+ end
+ end
+
+ def make_project_fully_accessible(project, accessible_project)
+ add_outbound_accessible_linkage(project, accessible_project)
+ add_inbound_accessible_linkage(project, accessible_project)
+ end
+
+ def add_outbound_accessible_linkage(project, accessible_project)
+ create(
+ :ci_job_token_project_scope_link,
+ source_project: project,
+ target_project: accessible_project,
+ direction: :outbound
+ )
+ end
+
+ def add_inbound_accessible_linkage(project, accessible_project)
+ create(
+ :ci_job_token_project_scope_link,
+ source_project: accessible_project,
+ target_project: project,
+ direction: :inbound
+ )
+ end
+ end
+end
diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb
index 2cdd242ac22..cd3ab4bd82d 100644
--- a/spec/support/helpers/ci/template_helpers.rb
+++ b/spec/support/helpers/ci/template_helpers.rb
@@ -13,14 +13,21 @@ module Ci
def public_image_manifest(registry, repository, reference)
token = public_image_repository_token(registry, repository)
+ headers = {
+ 'Authorization' => "Bearer #{token}",
+ 'Accept' => 'application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json'
+ }
response = with_net_connect_allowed do
- Gitlab::HTTP.get(image_manifest_url(registry, repository, reference),
- headers: { 'Authorization' => "Bearer #{token}" })
+ Gitlab::HTTP.get(image_manifest_url(registry, repository, reference), headers: headers)
end
- return unless response.success?
-
- Gitlab::Json.parse(response.body)
+ if response.success?
+ Gitlab::Json.parse(response.body)
+ elsif response.not_found?
+ nil
+ else
+ raise "Could not retrieve manifest: #{response.body}"
+ end
end
def public_image_repository_token(registry, repository)
@@ -31,17 +38,17 @@ module Ci
Gitlab::HTTP.get(image_manifest_url(registry, repository, 'latest'))
end
- return unless response.unauthorized?
+ raise 'Unauthorized' unless response.unauthorized?
www_authenticate = response.headers['www-authenticate']
- return unless www_authenticate
+ raise 'Missing www-authenticate' unless www_authenticate
realm, service, scope = www_authenticate.split(',').map { |s| s[/\w+="(.*)"/, 1] }
token_response = with_net_connect_allowed do
Gitlab::HTTP.get(realm, query: { service: service, scope: scope })
end
- return unless token_response.success?
+ raise "Could not get token: #{token_response.body}" unless token_response.success?
token_response['token']
end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 632f3ea28ee..eba5771e062 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -185,7 +185,7 @@ module CycleAnalyticsHelpers
def merge_merge_requests_closing_issue(user, project, issue)
merge_requests = Issues::ReferencedMergeRequestsService
- .new(project: project, current_user: user)
+ .new(container: project, current_user: user)
.closed_by_merge_requests(issue)
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project: project, current_user: user, params: { sha: merge_request.diff_head_sha }).execute(merge_request) }
diff --git a/spec/support/helpers/database/multiple_databases_helpers.rb b/spec/support/helpers/database/multiple_databases_helpers.rb
index 16f5168ca29..5083ea1ff53 100644
--- a/spec/support/helpers/database/multiple_databases_helpers.rb
+++ b/spec/support/helpers/database/multiple_databases_helpers.rb
@@ -2,12 +2,28 @@
module Database
module MultipleDatabasesHelpers
- def skip_if_multiple_databases_not_setup
- skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci)
+ EXTRA_DBS = ::Gitlab::Database::DATABASE_NAMES.map(&:to_sym) - [:main]
+
+ def skip_if_multiple_databases_not_setup(*databases)
+ unless (databases - EXTRA_DBS).empty?
+ raise "Unsupported database in #{databases}. It must be one of #{EXTRA_DBS}."
+ end
+
+ databases = EXTRA_DBS if databases.empty?
+ return if databases.any? { |db| Gitlab::Database.has_config?(db) }
+
+ skip "Skipping because none of the extra databases #{databases} are setup"
end
- def skip_if_multiple_databases_are_setup
- skip 'Skipping because multiple databases are set up' if Gitlab::Database.has_config?(:ci)
+ def skip_if_multiple_databases_are_setup(*databases)
+ unless (databases - EXTRA_DBS).empty?
+ raise "Unsupported database in #{databases}. It must be one of #{EXTRA_DBS}."
+ end
+
+ databases = EXTRA_DBS if databases.empty?
+ return if databases.none? { |db| Gitlab::Database.has_config?(db) }
+
+ skip "Skipping because some of the extra databases #{databases} are setup"
end
def reconfigure_db_connection(name: nil, config_hash: {}, model: ActiveRecord::Base, config_model: nil)
@@ -71,6 +87,14 @@ module Database
end
# rubocop:enable Database/MultipleDatabases
+ def with_db_configs(test: test_config)
+ current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases
+ ActiveRecord::Base.configurations = { test: test_config }
+ yield
+ ensure
+ ActiveRecord::Base.configurations = current_configurations
+ end
+
def with_added_ci_connection
if Gitlab::Database.has_config?(:ci)
# No need to add a ci: connection if we already have one
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index d0f6fd466d0..f4bdaa7e425 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -56,4 +56,24 @@ module EmailHelpers
have_subject [prefix, suffix].compact.join
end
+
+ def enqueue_mail_with(mailer_class, mail_method_name, *args)
+ args.map! { |arg| arg.is_a?(ActiveRecord::Base) ? arg.id : arg }
+ have_enqueued_mail(mailer_class, mail_method_name).with(*args)
+ end
+
+ def not_enqueue_mail_with(mailer_class, mail_method_name, *args)
+ args.map! { |arg| arg.is_a?(ActiveRecord::Base) ? arg.id : arg }
+ not_enqueue_mail(mailer_class, mail_method_name).with(*args)
+ end
+
+ def have_only_enqueued_mail_with_args(mailer_class, mailer_method, *args)
+ raise ArgumentError, 'You must provide at least one array of mailer arguments' if args.empty?
+
+ count_expectation = have_enqueued_mail(mailer_class, mailer_method).exactly(args.size).times
+
+ args.inject(count_expectation) do |composed_expectation, arguments|
+ composed_expectation.and(have_enqueued_mail(mailer_class, mailer_method).with(*arguments))
+ end
+ end
end
diff --git a/spec/support/helpers/features/branches_helpers.rb b/spec/support/helpers/features/branches_helpers.rb
index d4f96718cc0..dc4fa448167 100644
--- a/spec/support/helpers/features/branches_helpers.rb
+++ b/spec/support/helpers/features/branches_helpers.rb
@@ -22,15 +22,10 @@ module Spec
end
def select_branch(branch_name)
- ref_selector = '.ref-selector'
- find(ref_selector).click
wait_for_requests
- 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
+ click_button branch_name
+ send_keys branch_name
end
end
end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index a24b99bbe61..545e12341ef 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -15,17 +15,18 @@ module Spec
module Helpers
module Features
module ReleasesHelpers
+ include ListboxHelpers
+
def select_new_tag_name(tag_name)
page.within '[data-testid="tag-name-field"]' do
find('button').click
-
wait_for_all_requests
find('input[aria-label="Search or create tag"]').set(tag_name)
-
wait_for_all_requests
click_button("Create tag #{tag_name}")
+ click_button tag_name
end
end
@@ -39,7 +40,7 @@ module Spec
wait_for_all_requests
- click_button(branch_name.to_s)
+ select_listbox_item(branch_name.to_s, exact_text: true)
end
end
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index 50b8083ebb3..504a9b764cf 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -26,8 +26,8 @@ module Spec
# all of the dropdowns are converted, pajamas_sort_by can be renamed to sort_by
# https://gitlab.com/groups/gitlab-org/-/epics/7551
def pajamas_sort_by(value)
- find('.filter-dropdown-container .dropdown').click
- find('.dropdown-item', text: value).click
+ find('.filter-dropdown-container .gl-new-dropdown').click
+ find('.gl-new-dropdown-item', text: value).click
end
end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 20c104cd85c..398a2a20f2f 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -65,6 +65,7 @@ module GitalySetup
def env
{
'GEM_PATH' => Gem.path.join(':'),
+ 'BUNDLER_SETUP' => nil,
'BUNDLE_INSTALL_FLAGS' => nil,
'BUNDLE_IGNORE_CONFIG' => '1',
'BUNDLE_PATH' => bundle_path,
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 40eb46878ad..403456fa48e 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -24,7 +24,9 @@ module JavaScriptFixturesHelpers
# pick an arbitrary date from the past, so tests are not time dependent
# Also see spec/frontend/__helpers__/fake_date/jest.js
- travel_to(Time.utc(2015, 7, 3, 10)) { example.run }
+ travel_to Time.utc(2015, 7, 3, 10)
+ example.run
+ travel_back
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/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 72524453f34..c3076a2c359 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -534,10 +534,10 @@ module KubernetesHelpers
}
end
- def kube_knative_services_body(**options)
+ def kube_knative_services_body(...)
{
"kind" => "List",
- "items" => [knative_09_service(**options)]
+ "items" => [knative_09_service(...)]
}
end
diff --git a/spec/support/helpers/listbox_helpers.rb b/spec/support/helpers/listbox_helpers.rb
index 5fcd05f31fb..e943790fc65 100644
--- a/spec/support/helpers/listbox_helpers.rb
+++ b/spec/support/helpers/listbox_helpers.rb
@@ -7,18 +7,18 @@ module ListboxHelpers
end
def select_listbox_item(text, exact_text: false)
- find('.gl-listbox-item[role="option"]', text: text, exact_text: exact_text).click
+ find('.gl-new-dropdown-item[role="option"]', text: text, exact_text: exact_text).click
end
def expect_listbox_item(text)
- expect(page).to have_css('.gl-listbox-item[role="option"]', text: text)
+ expect(page).to have_css('.gl-new-dropdown-item[role="option"]', text: text)
end
def expect_no_listbox_item(text)
- expect(page).not_to have_css('.gl-listbox-item[role="option"]', text: text)
+ expect(page).not_to have_css('.gl-new-dropdown-item[role="option"]', text: text)
end
def expect_listbox_items(items)
- expect(find_all('.gl-listbox-item[role="option"]').map(&:text)).to eq(items)
+ expect(find_all('.gl-new-dropdown-item[role="option"]').map(&:text)).to eq(items)
end
end
diff --git a/spec/support/helpers/reload_helpers.rb b/spec/support/helpers/reload_helpers.rb
index 368ebaaba8a..71becd535b0 100644
--- a/spec/support/helpers/reload_helpers.rb
+++ b/spec/support/helpers/reload_helpers.rb
@@ -5,8 +5,8 @@ module ReloadHelpers
models.compact.map(&:reload)
end
- def subject_and_reload(*models)
+ def subject_and_reload(...)
subject
- reload_models(*models)
+ reload_models(...)
end
end
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
deleted file mode 100644
index 38bf34bdd61..00000000000
--- a/spec/support/helpers/select2_helper.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'wait_for_requests'
-
-# Select2 ajax programmatic helper
-# It allows you to select value from select2
-#
-# Params
-# value - real value of selected item
-# opts - options containing css selector
-#
-# Usage:
-#
-# select2(2, from: '#user_ids')
-#
-
-module Select2Helper
- include WaitForRequests
-
- def select2(value, options = {})
- raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
-
- wait_for_requests unless options[:async]
-
- selector = options.fetch(:from)
-
- ensure_select2_loaded(selector)
-
- if options[:multiple]
- execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');")
- else
- execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
- end
- end
-
- def open_select2(selector)
- ensure_select2_loaded(selector)
-
- execute_script("$('#{selector}').select2('open');")
- end
-
- def close_select2(selector)
- ensure_select2_loaded(selector)
-
- execute_script("$('#{selector}').select2('close');")
- end
-
- def scroll_select2_to_bottom(selector)
- evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
- end
-
- private
-
- def ensure_select2_loaded(selector)
- first(selector, visible: :all).sibling('.select2-container')
- end
-end
diff --git a/spec/support/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb
index 5f344f8fb52..afa501d6279 100644
--- a/spec/support/helpers/stub_env.rb
+++ b/spec/support/helpers/stub_env.rb
@@ -2,13 +2,23 @@
# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
module StubENV
+ # Stub ENV variables
+ #
+ # You can provide either a key and value as separate params or both in a Hash format
+ #
+ # Keys and values will always be converted to String, to comply with how ENV behaves
+ #
+ # @param key_or_hash [String, Hash<String,String>]
+ # @param value [String]
def stub_env(key_or_hash, value = nil)
init_stub unless env_stubbed?
if key_or_hash.is_a? Hash
- key_or_hash.each { |k, v| add_stubbed_value(k, v) }
+ key_or_hash.each do |key, value|
+ add_stubbed_value(key, ensure_env_type(value))
+ end
else
- add_stubbed_value key_or_hash, value
+ add_stubbed_value key_or_hash, ensure_env_type(value)
end
end
@@ -35,4 +45,8 @@ module StubENV
allow(ENV).to receive(:fetch).and_call_original
add_stubbed_value(STUBBED_KEY, true)
end
+
+ def ensure_env_type(value)
+ value.nil? ? value : value.to_s
+ end
end
diff --git a/spec/support/helpers/stubbed_member.rb b/spec/support/helpers/stubbed_member.rb
index 27420c9b709..d61cdea5354 100644
--- a/spec/support/helpers/stubbed_member.rb
+++ b/spec/support/helpers/stubbed_member.rb
@@ -11,9 +11,7 @@ module StubbedMember
module Member
private
- def refresh_member_authorized_projects(blocking:)
- return super unless blocking
-
+ def refresh_member_authorized_projects
AuthorizedProjectsWorker.new.perform(user_id)
end
end
@@ -21,7 +19,7 @@ module StubbedMember
module ProjectMember
private
- def blocking_project_authorizations_refresh
+ def execute_project_authorizations_refresh
AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.new.perform(project.id, user.id)
end
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 3530d1b1a39..3403064bf0b 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -423,7 +423,7 @@ module TestEnv
return if File.exist?(install_dir) && ci?
if component_needs_update?(install_dir, version)
- puts "==> Starting #{component} set up...\n"
+ puts "==> Starting #{component} (#{version}) set up...\n"
# Cleanup the component entirely to ensure we start fresh
FileUtils.rm_rf(install_dir) if fresh_install
diff --git a/spec/support/helpers/trial_status_widget_test_helper.rb b/spec/support/helpers/trial_status_widget_test_helper.rb
deleted file mode 100644
index d75620d17ee..00000000000
--- a/spec/support/helpers/trial_status_widget_test_helper.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module TrialStatusWidgetTestHelper
- def purchase_href(group)
- new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id')
- end
-end
-
-TrialStatusWidgetTestHelper.prepend_mod
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 438f0d129b9..2bec945fbc8 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -4,7 +4,6 @@ module UsageDataHelpers
COUNTS_KEYS = %i(
assignee_lists
ci_builds
- ci_internal_pipelines
ci_external_pipelines
ci_pipeline_config_auto_devops
ci_pipeline_config_repository
@@ -110,7 +109,6 @@ module UsageDataHelpers
gitaly
database
prometheus_metrics_enabled
- web_ide_clientside_preview_enabled
object_store
topology
).freeze
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 3d7a0d29e71..f8f32fa59d1 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -51,22 +51,22 @@ module ImportExport
json
end
- def restore_then_save_project(project, import_path:, export_path:)
- project_restorer = get_project_restorer(project, import_path)
- project_saver = get_project_saver(project, export_path)
+ def restore_then_save_project(project, user, import_path:, export_path:)
+ project_restorer = get_project_restorer(project, user, import_path)
+ project_saver = get_project_saver(project, user, export_path)
project_restorer.restore && project_saver.save
end
- def get_project_restorer(project, import_path)
+ def get_project_restorer(project, user, import_path)
Gitlab::ImportExport::Project::TreeRestorer.new(
- user: project.creator, shared: get_shared_env(path: import_path), project: project
+ user: user, shared: get_shared_env(path: import_path), project: project
)
end
- def get_project_saver(project, export_path)
+ def get_project_saver(project, user, export_path)
Gitlab::ImportExport::Project::TreeSaver.new(
- project: project, current_user: project.creator, shared: get_shared_env(path: export_path)
+ project: project, current_user: user, shared: get_shared_env(path: export_path)
)
end
diff --git a/spec/support/import_export/project_tree_expectations.rb b/spec/support/import_export/project_tree_expectations.rb
index 2423a58a3e6..0049d0fbd06 100644
--- a/spec/support/import_export/project_tree_expectations.rb
+++ b/spec/support/import_export/project_tree_expectations.rb
@@ -59,7 +59,7 @@ module ImportExport
end
def match_arrays(left_node, right_node, stats, location_stack, failures)
- has_simple_elements = left_node.none? { |el| Enumerable === el }
+ has_simple_elements = left_node.none?(Enumerable)
# for simple types, we can do a direct order-less set comparison
if has_simple_elements && left_node.to_set != right_node.to_set
stats[:arrays][:direct] += 1
diff --git a/spec/support/matchers/not_enqueue_mail_matcher.rb b/spec/support/matchers/not_enqueue_mail_matcher.rb
new file mode 100644
index 00000000000..0975c038252
--- /dev/null
+++ b/spec/support/matchers/not_enqueue_mail_matcher.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define_negated_matcher :not_enqueue_mail, :have_enqueued_mail
diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb
index d2f32b60464..d5a07f200dd 100644
--- a/spec/support/matchers/schema_matcher.rb
+++ b/spec/support/matchers/schema_matcher.rb
@@ -16,20 +16,8 @@ module SchemaPath
end
def self.validator(schema_path)
- unless @schema_cache.key?(schema_path)
- @schema_cache[schema_path] = JSONSchemer.schema(schema_path, ref_resolver: SchemaPath.file_ref_resolver)
- end
-
- @schema_cache[schema_path]
- end
-
- def self.file_ref_resolver
- proc do |uri|
- file = Rails.root.join(uri.path)
- raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json')
- raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist?
-
- Gitlab::Json.parse(File.read(file))
+ @schema_cache.fetch(schema_path) do
+ @schema_cache[schema_path] = JSONSchemer.schema(schema_path)
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 3a79ed1b5a9..4107bbcb976 100644
--- a/spec/support/models/ci/partitioning_testing/schema_helpers.rb
+++ b/spec/support/models/ci/partitioning_testing/schema_helpers.rb
@@ -24,13 +24,13 @@ module Ci
each_partitionable_table do |table_name|
change_column_default(table_name, from: DEFAULT_PARTITION, to: nil, connection: connection)
change_column_default("p_#{table_name}", from: DEFAULT_PARTITION, to: nil, connection: connection)
- create_test_partition(table_name, connection: connection)
+ create_test_partition("p_#{table_name}", connection: connection)
end
end
def teardown(connection: Ci::ApplicationRecord.connection)
each_partitionable_table do |table_name|
- drop_test_partition(table_name, connection: connection)
+ drop_test_partition("p_#{table_name}", connection: connection)
change_column_default(table_name, from: nil, to: DEFAULT_PARTITION, connection: connection)
change_column_default("p_#{table_name}", from: nil, to: DEFAULT_PARTITION, connection: connection)
end
@@ -54,13 +54,13 @@ module Ci
end
def create_test_partition(table_name, connection:)
- return unless table_available?("p_#{table_name}", connection: connection)
+ return unless table_available?(table_name, connection: connection)
drop_test_partition(table_name, connection: connection)
- connection.execute(<<~SQL)
+ connection.execute(<<~SQL.squish)
CREATE TABLE #{full_partition_name(table_name)}
- PARTITION OF p_#{table_name}
+ PARTITION OF #{table_name}
FOR VALUES IN (#{PartitioningTesting::PartitionIdentifiers.ci_testing_partition_id});
SQL
end
@@ -68,7 +68,7 @@ module Ci
def drop_test_partition(table_name, connection:)
return unless table_available?(table_name, connection: connection)
- connection.execute(<<~SQL)
+ connection.execute(<<~SQL.squish)
DROP TABLE IF EXISTS #{full_partition_name(table_name)};
SQL
end
@@ -79,7 +79,12 @@ module Ci
end
def full_partition_name(table_name)
- "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_#{table_name}_partition"
+ [
+ Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA,
+ '._test_gitlab_',
+ table_name.delete_prefix('p_'),
+ '_partition'
+ ].join('')
end
end
end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index 6d313c8aa16..d5ae0bf1582 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -25,4 +25,10 @@ RSpec.configure do |config|
instance_class.with(&:flushdb)
end
end
+
+ config.before(:each, :use_null_store_as_repository_cache) do |example|
+ null_store = ActiveSupport::Cache::NullStore.new
+
+ allow(Gitlab::Redis::RepositoryCache).to receive(:cache_store).and_return(null_store)
+ end
end
diff --git a/spec/support/redis/redis_new_instance_shared_examples.rb b/spec/support/redis/redis_new_instance_shared_examples.rb
index 0f2de78b2cb..435d342fcca 100644
--- a/spec/support/redis/redis_new_instance_shared_examples.rb
+++ b/spec/support/redis/redis_new_instance_shared_examples.rb
@@ -27,38 +27,34 @@ RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_cl
FileUtils.mkdir_p(File.join(rails_root, 'config'))
end
- context 'when there is only a resque.yml' do
+ context 'and there is a global env override' do
before do
- FileUtils.touch(File.join(rails_root, 'config/resque.yml'))
+ stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
end
- it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
+ it { expect(subject).to eq('global override') }
- context 'and there is a global env override' do
- before do
- stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
- end
-
- it { expect(subject).to eq('global override') }
+ context "and #{fallback_class.name.demodulize} has a different config file" do
+ let(:fallback_config_file) { 'fallback config file' }
- context "and #{fallback_class.name.demodulize} has a different config file" do
- let(:fallback_config_file) { 'fallback config file' }
-
- it { expect(subject).to eq('fallback config file') }
- end
+ it { expect(subject).to eq('fallback config file') }
end
end
end
describe '#fetch_config' do
- context 'when redis.yml exists' do
- subject { described_class.new('test').send(:fetch_config) }
+ subject { described_class.new('test').send(:fetch_config) }
+
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ end
+ context 'when redis.yml exists' do
before do
allow(described_class).to receive(:config_file_name).and_call_original
allow(described_class).to receive(:redis_yml_path).and_call_original
- allow(described_class).to receive(:rails_root).and_return(rails_root)
- FileUtils.mkdir_p(File.join(rails_root, 'config'))
end
context 'when the fallback has a redis.yml entry' do
@@ -93,5 +89,23 @@ RSpec.shared_examples "redis_new_instance_shared_examples" do |name, fallback_cl
end
end
end
+
+ context 'when no redis config file exsits' do
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when resque.yml exists' do
+ before do
+ File.write(File.join(rails_root, 'config/resque.yml'), {
+ 'test' => { 'foobar' => 123 }
+ }.to_json)
+ end
+
+ it 'returns the config from resque.yml' do
+ expect(subject).to eq({ 'foobar' => 123 })
+ end
+ end
+ end
end
end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 43c118a362d..8c195a9dbeb 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -40,42 +40,30 @@ RSpec.shared_examples "redis_shared_examples" do
context 'when there is no config file anywhere' do
it { expect(subject).to be_nil }
- context 'but resque.yml exists' do
+ context 'and there is a global env override' do
before do
- FileUtils.touch(File.join(rails_root, 'config', 'resque.yml'))
+ stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
end
- it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
-
- it 'returns a path that exists' do
- expect(File.file?(subject)).to eq(true)
- end
+ it { expect(subject).to eq('global override') }
- context 'and there is a global env override' do
+ context 'and there is an instance specific config file' do
before do
- stub_env('GITLAB_REDIS_CONFIG_FILE', 'global override')
+ FileUtils.touch(File.join(rails_root, instance_specific_config_file))
end
- it { expect(subject).to eq('global override') }
-
- context 'and there is an instance specific config file' do
- before do
- FileUtils.touch(File.join(rails_root, instance_specific_config_file))
- end
+ it { expect(subject).to eq("#{rails_root}/#{instance_specific_config_file}") }
- it { expect(subject).to eq("#{rails_root}/#{instance_specific_config_file}") }
+ it 'returns a path that exists' do
+ expect(File.file?(subject)).to eq(true)
+ end
- it 'returns a path that exists' do
- expect(File.file?(subject)).to eq(true)
+ context 'and there is a specific env override' do
+ before do
+ stub_env(environment_config_file_name, 'instance specific override')
end
- context 'and there is a specific env override' do
- before do
- stub_env(environment_config_file_name, 'instance specific override')
- end
-
- it { expect(subject).to eq('instance specific override') }
- end
+ it { expect(subject).to eq('instance specific override') }
end
end
end
@@ -402,6 +390,13 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '#fetch_config' do
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ # Undo top-level stub of config_file_name because we are testing that method now.
+ allow(described_class).to receive(:config_file_name).and_call_original
+ allow(described_class).to receive(:rails_root).and_return(rails_root)
+ end
+
it 'raises an exception when the config file contains invalid yaml' do
Tempfile.open('bad.yml') do |file|
file.write('{"not":"yaml"')
@@ -422,10 +417,7 @@ RSpec.shared_examples "redis_shared_examples" do
subject { described_class.new('test').send(:fetch_config) }
before do
- allow(described_class).to receive(:config_file_name).and_call_original
allow(described_class).to receive(:redis_yml_path).and_call_original
- allow(described_class).to receive(:rails_root).and_return(rails_root)
- FileUtils.mkdir_p(File.join(rails_root, 'config'))
end
it 'uses config/redis.yml' do
@@ -436,6 +428,27 @@ RSpec.shared_examples "redis_shared_examples" do
expect(subject).to eq({ 'foobar' => 123 })
end
end
+
+ context 'when no config file exsits' do
+ subject { described_class.new('test').send(:fetch_config) }
+
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'but resque.yml exists' do
+ before do
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ File.write(File.join(rails_root, 'config/resque.yml'), {
+ 'test' => { 'foobar' => 123 }
+ }.to_json)
+ end
+
+ it 'returns the config from resque.yml' do
+ expect(subject).to eq({ 'foobar' => 123 })
+ end
+ end
+ end
end
def clear_pool
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 2f3f0feb87e..7aa7d8e8abd 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -165,7 +165,6 @@
- './ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb'
- './ee/spec/controllers/registrations/company_controller_spec.rb'
- './ee/spec/controllers/registrations/groups_projects_controller_spec.rb'
-- './ee/spec/controllers/registrations/verification_controller_spec.rb'
- './ee/spec/controllers/repositories/git_http_controller_spec.rb'
- './ee/spec/controllers/security/dashboard_controller_spec.rb'
- './ee/spec/controllers/security/projects_controller_spec.rb'
@@ -341,7 +340,6 @@
- './ee/spec/features/issues/form_spec.rb'
- './ee/spec/features/issues/gfm_autocomplete_ee_spec.rb'
- './ee/spec/features/issues/issue_actions_spec.rb'
-- './ee/spec/features/issues/issue_sidebar_spec.rb'
- './ee/spec/features/issues/move_issue_resource_weight_events_spec.rb'
- './ee/spec/features/issues/related_issues_spec.rb'
- './ee/spec/features/issues/resource_weight_events_spec.rb'
@@ -428,7 +426,6 @@
- './ee/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb'
- './ee/spec/features/projects/milestones/milestone_spec.rb'
- './ee/spec/features/projects/mirror_spec.rb'
-- './ee/spec/features/projects/navbar_spec.rb'
- './ee/spec/features/projects/new_project_from_template_spec.rb'
- './ee/spec/features/projects/new_project_spec.rb'
- './ee/spec/features/projects/path_locks_spec.rb'
@@ -966,11 +963,9 @@
- './ee/spec/helpers/ee/groups/settings_helper_spec.rb'
- './ee/spec/helpers/ee/hooks_helper_spec.rb'
- './ee/spec/helpers/ee/integrations_helper_spec.rb'
-- './ee/spec/helpers/ee/invite_members_helper_spec.rb'
- './ee/spec/helpers/ee/issuables_helper_spec.rb'
- './ee/spec/helpers/ee/issues_helper_spec.rb'
- './ee/spec/helpers/ee/labels_helper_spec.rb'
-- './ee/spec/helpers/ee/learn_gitlab_helper_spec.rb'
- './ee/spec/helpers/ee/lock_helper_spec.rb'
- './ee/spec/helpers/ee/namespaces_helper_spec.rb'
- './ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb'
@@ -1006,10 +1001,7 @@
- './ee/spec/helpers/manual_quarterly_co_term_banner_helper_spec.rb'
- './ee/spec/helpers/markup_helper_spec.rb'
- './ee/spec/helpers/merge_requests_helper_spec.rb'
-- './ee/spec/helpers/nav/new_dropdown_helper_spec.rb'
-- './ee/spec/helpers/nav/top_nav_helper_spec.rb'
- './ee/spec/helpers/notes_helper_spec.rb'
-- './ee/spec/helpers/paid_feature_callout_helper_spec.rb'
- './ee/spec/helpers/path_locks_helper_spec.rb'
- './ee/spec/helpers/preferences_helper_spec.rb'
- './ee/spec/helpers/prevent_forking_helper_spec.rb'
@@ -1640,7 +1632,6 @@
- './ee/spec/models/allowed_email_domain_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/aggregation_context_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
-- './ee/spec/models/analytics/cycle_analytics/project_stage_spec.rb'
- './ee/spec/models/analytics/cycle_analytics/runtime_limiter_spec.rb'
- './ee/spec/models/analytics/devops_adoption/enabled_namespace_spec.rb'
- './ee/spec/models/analytics/devops_adoption/snapshot_spec.rb'
@@ -1674,7 +1665,6 @@
- './ee/spec/models/broadcast_message_spec.rb'
- './ee/spec/models/burndown_spec.rb'
- './ee/spec/models/ci/bridge_spec.rb'
-- './ee/spec/models/ci/build_spec.rb'
- './ee/spec/models/ci/daily_build_group_report_result_spec.rb'
- './ee/spec/models/ci/minutes/additional_pack_spec.rb'
- './ee/spec/models/ci/minutes/context_spec.rb'
@@ -1782,6 +1772,7 @@
- './ee/spec/models/ee/pages_deployment_spec.rb'
- './ee/spec/models/ee/personal_access_token_spec.rb'
- './ee/spec/models/ee/preloaders/group_policy_preloader_spec.rb'
+- './ee/spec/models/ee/project_spec.rb'
- './ee/spec/models/ee/project_authorization_spec.rb'
- './ee/spec/models/ee/project_group_link_spec.rb'
- './ee/spec/models/ee/project_setting_spec.rb'
@@ -1905,7 +1896,6 @@
- './ee/spec/models/project_member_spec.rb'
- './ee/spec/models/project_repository_state_spec.rb'
- './ee/spec/models/project_security_setting_spec.rb'
-- './ee/spec/models/project_spec.rb'
- './ee/spec/models/project_team_spec.rb'
- './ee/spec/models/protected_branch/required_code_owners_section_spec.rb'
- './ee/spec/models/protected_branch/unprotect_access_level_spec.rb'
@@ -3117,7 +3107,6 @@
- './ee/spec/views/groups/compliance_frameworks/new.html.haml_spec.rb'
- './ee/spec/views/groups/edit.html.haml_spec.rb'
- './ee/spec/views/groups/feature_discovery_moments/advanced_features_dashboard.html.haml_spec.rb'
-- './ee/spec/views/groups/group_members/index.html.haml_spec.rb'
- './ee/spec/views/groups/hook_logs/show.html.haml_spec.rb'
- './ee/spec/views/groups/hooks/edit.html.haml_spec.rb'
- './ee/spec/views/groups/security/discover/show.html.haml_spec.rb'
@@ -3128,7 +3117,6 @@
- './ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb'
- './ee/spec/views/layouts/header/_ee_subscribable_banner.html.haml_spec.rb'
- './ee/spec/views/layouts/header/help_dropdown/_cross_stage_fdm.html.haml_spec.rb'
-- './ee/spec/views/layouts/header/_new_dropdown.haml_spec.rb'
- './ee/spec/views/layouts/header/_read_only_banner.html.haml_spec.rb'
- './ee/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb'
- './ee/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb'
@@ -3142,7 +3130,6 @@
- './ee/spec/views/projects/issues/show.html.haml_spec.rb'
- './ee/spec/views/projects/_merge_request_status_checks_settings.html.haml_spec.rb'
- './ee/spec/views/projects/on_demand_scans/index.html.haml_spec.rb'
-- './ee/spec/views/projects/project_members/index.html.haml_spec.rb'
- './ee/spec/views/projects/security/corpus_management/show.html.haml_spec.rb'
- './ee/spec/views/projects/security/dast_profiles/show.html.haml_spec.rb'
- './ee/spec/views/projects/security/dast_scanner_profiles/edit.html.haml_spec.rb'
@@ -3154,7 +3141,6 @@
- './ee/spec/views/projects/security/sast_configuration/show.html.haml_spec.rb'
- './ee/spec/views/projects/settings/subscriptions/_index.html.haml_spec.rb'
- './ee/spec/views/registrations/groups_projects/new.html.haml_spec.rb'
-- './ee/spec/views/registrations/welcome/continuous_onboarding_getting_started.html.haml_spec.rb'
- './ee/spec/views/search/_category.html.haml_spec.rb'
- './ee/spec/views/shared/billings/_billing_plan_actions.html.haml_spec.rb'
- './ee/spec/views/shared/billings/_billing_plan.html.haml_spec.rb'
@@ -3549,7 +3535,6 @@
- './spec/controllers/projects/issues_controller_spec.rb'
- './spec/controllers/projects/jobs_controller_spec.rb'
- './spec/controllers/projects/labels_controller_spec.rb'
-- './spec/controllers/projects/learn_gitlab_controller_spec.rb'
- './spec/controllers/projects/mattermosts_controller_spec.rb'
- './spec/controllers/projects/merge_requests/conflicts_controller_spec.rb'
- './spec/controllers/projects/merge_requests/content_controller_spec.rb'
@@ -3635,7 +3620,6 @@
- './spec/experiments/ios_specific_templates_experiment_spec.rb'
- './spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb'
- './spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb'
-- './spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb'
- './spec/features/abuse_report_spec.rb'
- './spec/features/action_cable_logging_spec.rb'
- './spec/features/admin/admin_abuse_reports_spec.rb'
@@ -5162,7 +5146,6 @@
- './spec/helpers/import_helper_spec.rb'
- './spec/helpers/instance_configuration_helper_spec.rb'
- './spec/helpers/integrations_helper_spec.rb'
-- './spec/helpers/invite_members_helper_spec.rb'
- './spec/helpers/issuables_description_templates_helper_spec.rb'
- './spec/helpers/issuables_helper_spec.rb'
- './spec/helpers/issues_helper_spec.rb'
@@ -5237,9 +5220,7 @@
- './spec/helpers/wiki_helper_spec.rb'
- './spec/helpers/wiki_page_version_helper_spec.rb'
- './spec/helpers/x509_helper_spec.rb'
-- './spec/initializers/00_deprecations_spec.rb'
- './spec/initializers/00_rails_disable_joins_spec.rb'
-- './spec/initializers/0_log_deprecations_spec.rb'
- './spec/initializers/0_postgresql_types_spec.rb'
- './spec/initializers/100_patch_omniauth_oauth2_spec.rb'
- './spec/initializers/100_patch_omniauth_saml_spec.rb'
@@ -6130,7 +6111,6 @@
- './spec/lib/gitlab/ci/status/success_warning_spec.rb'
- './spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb'
- './spec/lib/gitlab/ci/tags/bulk_insert_spec.rb'
-- './spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb'
- './spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb'
- './spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb'
- './spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb'
@@ -7476,7 +7456,6 @@
- './spec/lib/sidebars/menu_spec.rb'
- './spec/lib/sidebars/panel_spec.rb'
- './spec/lib/sidebars/projects/context_spec.rb'
-- './spec/lib/sidebars/projects/menus/analytics_menu_spec.rb'
- './spec/lib/sidebars/projects/menus/ci_cd_menu_spec.rb'
- './spec/lib/sidebars/projects/menus/confluence_menu_spec.rb'
- './spec/lib/sidebars/projects/menus/deployments_menu_spec.rb'
@@ -7739,7 +7718,6 @@
- './spec/models/analytics/cycle_analytics/aggregation_spec.rb'
- './spec/models/analytics/cycle_analytics/issue_stage_event_spec.rb'
- './spec/models/analytics/cycle_analytics/merge_request_stage_event_spec.rb'
-- './spec/models/analytics/cycle_analytics/project_stage_spec.rb'
- './spec/models/analytics/cycle_analytics/project_value_stream_spec.rb'
- './spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb'
- './spec/models/analytics/usage_trends/measurement_spec.rb'
@@ -10309,7 +10287,6 @@
- './spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb'
- './spec/views/projects/pipelines/show.html.haml_spec.rb'
- './spec/views/projects/project_members/index.html.haml_spec.rb'
-- './spec/views/projects/runners/_specific_runners.html.haml_spec.rb'
- './spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb'
- './spec/views/projects/settings/integrations/edit.html.haml_spec.rb'
- './spec/views/projects/settings/operations/show.html.haml_spec.rb'
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index b85c3904127..feea21be428 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'issuable update service' do
expect(project).to receive(:execute_hooks).with(expected_payload, hook_event)
expect(project).to receive(:execute_integrations).with(expected_payload, hook_event)
- described_class.new(project: project, current_user: user, params: { state_event: 'reopen' }).execute(closed_issuable)
+ described_class.new(**described_class.constructor_container_arg(project), current_user: user, params: { state_event: 'reopen' }).execute(closed_issuable)
end
end
@@ -48,7 +48,7 @@ RSpec.shared_examples 'issuable update service' do
expect(project).to receive(:execute_hooks).with(expected_payload, hook_event)
expect(project).to receive(:execute_integrations).with(expected_payload, hook_event)
- described_class.new(project: project, current_user: user, params: { state_event: 'close' }).execute(open_issuable)
+ described_class.new(**described_class.constructor_container_arg(project), current_user: user, params: { state_event: 'close' }).execute(open_issuable)
end
end
end
diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
new file mode 100644
index 00000000000..1585ef0e7fc
--- /dev/null
+++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with FOSS query type fields' do
+ # extracted these fields into a shared variable so that we can define FOSS fields once and use them on EE spec as well
+ let(:expected_foss_fields) do
+ [
+ :board_list,
+ :ci_application_settings,
+ :ci_config,
+ :ci_variables,
+ :container_repository,
+ :current_user,
+ :design_management,
+ :echo,
+ :gitpod_enabled,
+ :group,
+ :groups,
+ :issue,
+ :issues,
+ :jobs,
+ :merge_request,
+ :metadata,
+ :milestone,
+ :namespace,
+ :note,
+ :package,
+ :project,
+ :projects,
+ :query_complexity,
+ :runner,
+ :runner_platforms,
+ :runner_setup,
+ :runners,
+ :snippets,
+ :synthetic_note,
+ :timelogs,
+ :todo,
+ :topics,
+ :usage_trends_measurements,
+ :user,
+ :users,
+ :work_item
+ ]
+ end
+end
diff --git a/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb b/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb
new file mode 100644
index 00000000000..4aa4d500f5c
--- /dev/null
+++ b/spec/support/shared_contexts/mailers/emails/service_desk_shared_context.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with service desk mailer' do
+ before do
+ stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
+
+ ServiceEmailClass.class_eval do
+ include GitlabRoutingHelper
+ include EmailsHelper
+ include Emails::ServiceDesk
+
+ helper GitlabRoutingHelper
+ helper EmailsHelper
+
+ # this method is implemented in Notify class, we don't need to test it
+ def reply_key
+ 'b7721fc7e8419911a8bea145236a0519'
+ end
+
+ # this method is implemented in Notify class, we don't need to test it
+ def sender(author_id, params = {})
+ author_id
+ end
+
+ # this method is implemented in Notify class
+ #
+ # We do not need to test the Notify method, it is already tested in notify_spec
+ def mail_new_thread(issue, options)
+ # we need to rewrite this in order to look up templates in the correct directory
+ self.class.mailer_name = 'notify'
+
+ # this is needed for default layout
+ @unsubscribe_url = 'http://unsubscribe.example.com'
+
+ mail(options)
+ end
+ alias_method :mail_answer_thread, :mail_new_thread
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb
index 51f671b139d..d0fee23b57c 100644
--- a/spec/support/shared_contexts/models/ci/job_token_scope.rb
+++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb
@@ -1,21 +1,27 @@
# 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) }
+RSpec.shared_context 'with a project in each allowlist' do
+ let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) }
+
+ include_context 'with inaccessible projects'
+end
+
+RSpec.shared_context 'with accessible and inaccessible projects' do
+ let_it_be(:outbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :outbound) }
+ let_it_be(:inbound_accessible_project) { create_inbound_accessible_project(source_project) }
+ let_it_be(:fully_accessible_project) { create_inbound_and_outbound_accessible_project(source_project) }
+
+ include_context 'with inaccessible projects'
+end
+
+RSpec.shared_context 'with inaccessible projects' do
+ let_it_be(:inbound_allowlist_project) { create_project_in_allowlist(source_project, direction: :inbound) }
+ include_context 'with unscoped projects'
+end
+
+RSpec.shared_context 'with unscoped projects' do
let_it_be(:unscoped_project1) { create(:project) }
let_it_be(:unscoped_project2) { create(:project) }
let_it_be(: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/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 9c7cf831241..b74819d2ac7 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -85,7 +85,8 @@ RSpec.shared_context 'project navbar structure' do
_('Metrics'),
_('Error Tracking'),
_('Alerts'),
- _('Incidents')
+ _('Incidents'),
+ _('Airflow')
]
},
{
@@ -243,7 +244,10 @@ RSpec.shared_context 'dashboard navbar structure' do
},
{
nav_item: _("Merge requests"),
- nav_sub_items: []
+ nav_sub_items: [
+ _('Assigned 0'),
+ _('Review requests 0')
+ ]
},
{
nav_item: _("To-Do List"),
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 fddcecbe125..4c081c8464e 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -36,6 +36,7 @@ RSpec.shared_context 'GroupPolicy context' do
read_prometheus
read_crm_contact
read_crm_organization
+ read_internal_note
]
end
@@ -81,7 +82,7 @@ RSpec.shared_context 'GroupPolicy context' do
]
end
- let(:admin_permissions) { %i[read_confidential_issues] }
+ let(:admin_permissions) { %i[read_confidential_issues read_internal_note] }
before_all do
group.add_guest(guest)
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 6e2caa853f8..afc7fc8766f 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,7 +38,8 @@ RSpec.shared_context 'ProjectPolicy context' do
read_commit_status read_confidential_issues read_container_image
read_harbor_registry read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
- read_sentry_issue update_issue create_merge_request_in
+ read_sentry_issue update_issue create_merge_request_in read_external_emails
+ read_internal_note
]
end
@@ -89,6 +90,13 @@ RSpec.shared_context 'ProjectPolicy context' do
]
end
+ let(:admin_permissions) do
+ %i[
+ read_project_for_iids update_max_artifacts_size read_storage_disk_path
+ owner_access admin_remote_mirror read_internal_note
+ ]
+ end
+
# Used in EE specs
let(:additional_guest_permissions) { [] }
let(:additional_reporter_permissions) { [] }
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 7c37e5189f1..f6e10543c84 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
@@ -11,7 +11,7 @@ RSpec.shared_context 'conan api setup' do
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let(:project) { package.project }
- let(:job) { create(:ci_build, :running, user: user) }
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
let(:job_token) { job.token }
let(:auth_token) { personal_access_token.token }
let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
diff --git a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
index cf090c7a185..57967fb9414 100644
--- a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb
@@ -75,7 +75,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
end
end
- let(:api_params) { workhorse_params }
+ let(:extra_params) { {} }
+ let(:api_params) { workhorse_params.merge(extra_params) }
let(:auth_headers) { {} }
let(:wh_headers) do
diff --git a/spec/support/shared_contexts/requests/api/graphql/releases_and_group_releases_shared_context.rb b/spec/support/shared_contexts/requests/api/graphql/releases_and_group_releases_shared_context.rb
new file mode 100644
index 00000000000..81076ea6fdc
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/graphql/releases_and_group_releases_shared_context.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when releases and group releases shared context' do
+ let_it_be(:stranger) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
+ let(:opened_url_params) { { state: 'opened', **base_url_params } }
+ let(:merged_url_params) { { state: 'merged', **base_url_params } }
+ let(:closed_url_params) { { state: 'closed', **base_url_params } }
+
+ let(:query) do
+ graphql_query_for(resource_type, { fullPath: resource.full_path },
+ %(
+ releases {
+ count
+ nodes {
+ tagName
+ tagPath
+ name
+ commit {
+ sha
+ }
+ assets {
+ count
+ sources {
+ nodes {
+ url
+ }
+ }
+ }
+ evidences {
+ nodes {
+ sha
+ }
+ }
+ links {
+ selfUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
+ }
+ }
+ }
+ ))
+ end
+
+ let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+
+ let(:data) { graphql_data.dig(resource_type.to_s, 'releases', 'nodes', 0) }
+
+ before do
+ stub_default_url_options(host: 'www.example.com')
+ end
+end
diff --git a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
new file mode 100644
index 00000000000..40e9726f89c
--- /dev/null
+++ b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'visibility level settings' do
+ context 'when public' do
+ let(:data) { { 'visibility' => 'public' } }
+
+ context 'when destination is a public group' do
+ let(:destination_group) { create(:group, :public) }
+
+ it 'sets visibility level to public' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context 'when destination is a internal group' do
+ let(:destination_group) { create(:group, :internal) }
+
+ it 'sets visibility level to internal' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'when destination is a private group' do
+ let(:destination_group) { create(:group, :private) }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when destination is blank' do
+ let(:destination_namespace) { '' }
+
+ it 'sets visibility level to public' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+ end
+
+ context 'when internal' do
+ let(:data) { { 'visibility' => 'internal' } }
+
+ context 'when destination is a public group' do
+ let(:destination_group) { create(:group, :public) }
+
+ it 'sets visibility level to internal' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'when destination is a internal group' do
+ let(:destination_group) { create(:group, :internal) }
+
+ it 'sets visibility level to internal' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+
+ context 'when destination is a private group' do
+ let(:destination_group) { create(:group, :private) }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when destination is blank' do
+ let(:destination_namespace) { '' }
+
+ it 'sets visibility level to internal' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ context 'when visibility level is restricted' do
+ it 'sets visibility level to private' do
+ stub_application_setting(
+ restricted_visibility_levels: [
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC
+ ]
+ )
+
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+ end
+
+ context 'when private' do
+ let(:data) { { 'visibility' => 'private' } }
+
+ context 'when destination is a public group' do
+ let(:destination_group) { create(:group, :public) }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when destination is a internal group' do
+ let(:destination_group) { create(:group, :internal) }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when destination is a private group' do
+ let(:destination_group) { create(:group, :private) }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'when destination is blank' do
+ let(:destination_namespace) { '' }
+
+ it 'sets visibility level to private' do
+ expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb b/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb
deleted file mode 100644
index e77acb93798..00000000000
--- a/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'issuable list with anonymous search disabled' do |action|
- let(:controller_action) { :index }
- let(:params_with_search) { params.merge(search: 'some search term') }
-
- context 'when disable_anonymous_search is enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'shows a flash message' do
- get controller_action, params: params_with_search
-
- expect(flash.now[:notice]).to eq('You must sign in to search for specific terms.')
- end
-
- context 'when search param is not given' do
- it 'does not show a flash message' do
- get controller_action, params: params
-
- expect(flash.now[:notice]).to be_nil
- end
- end
-
- context 'when user is signed-in' do
- it 'does not show a flash message' do
- sign_in(create(:user))
- get controller_action, params: params_with_search
-
- expect(flash.now[:notice]).to be_nil
- end
- end
-
- context 'when format is not HTML' do
- it 'does not show a flash message' do
- get controller_action, params: params_with_search.merge(format: :atom)
-
- expect(flash.now[:notice]).to be_nil
- end
- end
- end
-
- context 'when disable_anonymous_search is disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it 'does not show a flash message' do
- get controller_action, params: params_with_search
-
- expect(flash.now[:notice]).to be_nil
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
index 3a7588a5cc9..cc28a79b4ca 100644
--- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
@@ -61,9 +61,14 @@ RSpec.shared_examples Repositories::GitHttpController do
end
it 'updates the user activity' do
- expect_next_instance_of(Users::ActivityService) do |activity_service|
- expect(activity_service).to receive(:execute)
- end
+ activity_project = container.is_a?(PersonalSnippet) ? nil : project
+
+ activity_service = instance_double(Users::ActivityService)
+
+ args = { author: user, project: activity_project, namespace: activity_project&.namespace }
+ expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service)
+
+ expect(activity_service).to receive(:execute)
get :info_refs, params: params
end
diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
index 7e99066110d..ba00e3e0610 100644
--- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb
@@ -52,3 +52,12 @@ RSpec.shared_examples 'Snowplow event tracking with RedisHLL context' do |overri
end
end
end
+
+RSpec.shared_examples 'Snowplow event tracking with Redis context' do |overrides: {}|
+ it_behaves_like 'Snowplow event tracking', overrides: overrides do
+ let(:context) do
+ key_path = try(:label) || action
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: key_path).to_context.to_json]
+ 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 7f31ea8f9be..96e57980c68 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
@@ -2,6 +2,7 @@
RSpec.shared_examples 'a creatable merge request' do
include WaitForRequests
+ include ListboxHelpers
it 'creates new merge request', :js do
find('.js-assignee-search').click
@@ -64,14 +65,16 @@ 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 li', text: target_project.full_path).click
+ select_listbox_item(target_project.full_path)
wait_for_requests
first('.js-target-branch').click
- within('.js-target-branch-dropdown .dropdown-content') do
- expect(page).to have_content('a-brand-new-branch-to-test')
- end
+ find('.gl-listbox-search-input').set('a-brand-new-branch-to-test')
+
+ wait_for_requests
+
+ expect_listbox_item('a-brand-new-branch-to-test')
end
end
diff --git a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
new file mode 100644
index 00000000000..dab125caa60
--- /dev/null
+++ b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'for each incident details route' do |example, tab_text:|
+ before do
+ sign_in(user)
+ visit incident_path
+ end
+
+ context 'for /-/issues/:id route' do
+ let(:incident_path) { project_issue_path(project, incident) }
+
+ before do
+ page.within('[data-testid="incident-tabs"]') { click_link tab_text }
+ end
+
+ it_behaves_like example
+ end
+
+ context 'for /-/issues/incident/:id route' do
+ let(:incident_path) { incident_project_issues_path(project, incident) }
+
+ before do
+ page.within('[data-testid="incident-tabs"]') { click_link tab_text }
+ end
+
+ it_behaves_like example
+ end
+end
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index 149486320ae..b6f7094e422 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable invite members' do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
context 'when a privileged user can invite' do
before do
project.add_maintainer(user)
@@ -21,7 +23,9 @@ RSpec.shared_examples 'issuable invite members' do
click_link 'Invite Members'
- expect(page).to have_content("You're inviting members to the")
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{project.name} project")
+ end
end
end
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 9d859403465..bb3fab5b23e 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 administrator', href: abuse_report_path)
+ expect(dropdown).to have_button('Report abuse to administrator')
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,10 +33,14 @@ RSpec.shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report abuse to administrator')
+ dropdown.click_button('Report abuse to administrator')
+
+ choose "They're posting spam."
+ click_button "Next"
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_reported_from_url')['value']).to match(noteable_note_url(note))
+ expect(find('#abuse_report_category', visible: false)['value']).to match('spam')
end
def open_dropdown(dropdown)
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 20078243cfb..63a0832117d 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -178,6 +178,22 @@ RSpec.shared_examples 'filters by tag' do
end
end
+RSpec.shared_examples 'shows runner jobs tab' do
+ context 'when clicking on jobs tab' do
+ before do
+ click_on("#{s_('Runners|Jobs')} #{job_count}")
+
+ wait_for_requests
+ end
+
+ it 'shows job in list' do
+ within "[data-testid='job-row-#{job.id}']" do
+ expect(page).to have_link("##{job.id}")
+ end
+ end
+ end
+end
+
RSpec.shared_examples 'submits edit runner form' do
it 'breadcrumb contains runner id and token' do
page.within '[data-testid="breadcrumb-links"]' do
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 95b306fdaaa..a332fdec963 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
@@ -95,7 +95,7 @@ RSpec.shared_examples 'labels sidebar widget' do
end
end
- it 'creates new label' do
+ it 'creates new label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391240' do
page.within(labels_widget) do
fill_in 'Name new label', with: 'wontfix'
page.find('.suggest-colors a', match: :first).click
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index 77334db6a36..c2c50e8762f 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -42,6 +42,26 @@ RSpec.shared_examples 'issue boards sidebar' do
end
end
+ context 'editing issue title' do
+ it 'edits issue title' do
+ page.within('[data-testid="sidebar-title"]') do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find('input').set('Test title')
+
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(page).to have_content('Test title')
+ end
+
+ expect(first_card).to have_content('Test title')
+ end
+ end
+
context 'editing issue milestone', :js do
it_behaves_like 'milestone sidebar widget'
end
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
new file mode 100644
index 00000000000..4f3d957ad71
--- /dev/null
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work items status' do
+ let(:state_selector) { '[data-testid="work-item-state-select"]' }
+
+ it 'successfully shows and changes the status of the work item' do
+ expect(find(state_selector)).to have_content 'Open'
+
+ find(state_selector).select("Closed")
+
+ wait_for_requests
+
+ expect(find(state_selector)).to have_content 'Closed'
+ expect(work_item.reload.state).to eq('closed')
+ end
+end
+
+RSpec.shared_examples 'work items comments' do
+ let(:form_selector) { '[data-testid="work-item-add-comment"]' }
+
+ it 'successfully creates and shows comments' do
+ click_button 'Add a comment'
+
+ find(form_selector).fill_in(with: "Test comment")
+ click_button "Comment"
+
+ wait_for_requests
+
+ expect(page).to have_content "Test comment"
+ end
+end
+
+RSpec.shared_examples 'work items assignees' do
+ it 'successfully assigns the current user by searching' do
+ # The button is only when the mouse is over the input
+ find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username)
+ wait_for_requests
+
+ # submit and simulate blur to save
+ send_keys(:enter)
+ find("body").click
+
+ wait_for_requests
+
+ expect(work_item.assignees).to include(user)
+ end
+end
+
+RSpec.shared_examples 'work items labels' do
+ it 'successfully assigns a label' do
+ label = create(:label, project: work_item.project, title: "testing-label")
+
+ find('[data-testid="work-item-labels-input"]').fill_in(with: label.title)
+ wait_for_requests
+
+ # submit and simulate blur to save
+ send_keys(:enter)
+ find("body").click
+
+ wait_for_requests
+
+ expect(work_item.labels).to include(label)
+ end
+end
+
+RSpec.shared_examples 'work items description' do
+ it 'shows GFM autocomplete', :aggregate_failures do
+ click_button "Edit description"
+
+ find('[aria-label="Description"]').send_keys("@#{user.username}")
+
+ wait_for_requests
+
+ page.within('.atwho-container') do
+ expect(page).to have_text(user.name)
+ end
+ end
+
+ it 'autocompletes available quick actions', :aggregate_failures do
+ click_button "Edit description"
+
+ find('[aria-label="Description"]').send_keys("/")
+
+ wait_for_requests
+
+ page.within('.atwho-container') do
+ expect(page).to have_text("title")
+ expect(page).to have_text("shrug")
+ expect(page).to have_text("tableflip")
+ expect(page).to have_text("close")
+ expect(page).to have_text("cc")
+ end
+ end
+
+ context 'on conflict' do
+ let_it_be(:other_user) { create(:user) }
+ let(:expected_warning) { 'Someone edited the description at the same time you did.' }
+
+ before do
+ project.add_developer(other_user)
+ end
+
+ it 'shows conflict message when description changes', :aggregate_failures do
+ click_button "Edit description"
+
+ wait_for_requests
+
+ ::WorkItems::UpdateService.new(
+ container: work_item.project,
+ current_user: other_user,
+ params: { description: "oh no!" }
+ ).execute(work_item)
+
+ wait_for_requests
+
+ find('[aria-label="Description"]').send_keys("oh yeah!")
+
+ expect(page.find('[data-testid="work-item-description-conflicts"]')).to have_text(expected_warning)
+
+ click_button "Save and overwrite"
+
+ expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")
+ end
+ end
+end
+
+RSpec.shared_examples 'work items invite members' do
+ include Spec::Support::Helpers::Features::InviteMembersModalHelper
+
+ it 'successfully assigns the current user by searching' do
+ # The button is only when the mouse is over the input
+ find('[data-testid="work-item-assignees-input"]').fill_in(with: 'Invite members')
+ wait_for_requests
+
+ click_button('Invite members')
+
+ page.within invite_modal_selector do
+ expect(page).to have_content("You're inviting members to the #{work_item.project.name} project")
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 6f4072ba762..93f9e42241b 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -670,35 +670,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
expect(items).to contain_exactly(english)
end
end
-
- context 'with anonymous user' do
- let_it_be(:public_project) { create(:project, :public, group: subgroup) }
- let_it_be(:item6) { create(factory, project: public_project, title: 'tanuki') }
- let_it_be(:item7) { create(factory, project: public_project, title: 'ikunat') }
-
- let(:search_user) { nil }
- let(:params) { { search: 'tanuki' } }
-
- context 'with disable_anonymous_search feature flag enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'does not perform search' do
- expect(items).to contain_exactly(item6, item7)
- end
- end
-
- context 'with disable_anonymous_search feature flag disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it 'finds one public item' do
- expect(items).to contain_exactly(item6)
- end
- end
- end
end
context 'filtering by item term in title' do
@@ -1003,7 +974,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
let(:params) { { issue_types: ['nonsense'] } }
it 'returns no items' do
- expect(items).to eq(items_model.none)
+ expect(items.none?).to eq(true)
end
end
end
@@ -1285,28 +1256,12 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
subject { described_class.new(nil, params).with_confidentiality_access_check }
it_behaves_like 'returns public, does not return hidden or confidential'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
end
context 'for a user without project membership' do
subject { described_class.new(user, params).with_confidentiality_access_check }
it_behaves_like 'returns public, does not return hidden or confidential'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
end
context 'for a guest user' do
@@ -1317,28 +1272,12 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
it_behaves_like 'returns public, does not return hidden or confidential'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
end
context 'for a project member with access to view confidential items' do
subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
it_behaves_like 'returns public and confidential, does not return hidden'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public, confidential, and hidden'
- end
end
context 'for an admin' do
@@ -1348,26 +1287,10 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
context 'when admin mode is enabled', :enable_admin_mode do
it_behaves_like 'returns public, confidential, and hidden'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public, confidential, and hidden'
- end
end
context 'when admin mode is disabled' do
it_behaves_like 'returns public, does not return hidden or confidential'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
end
end
end
@@ -1380,14 +1303,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
it_behaves_like 'returns public, does not return hidden or confidential'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
-
it 'does not filter by confidentiality' do
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
subject
@@ -1399,14 +1314,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
it_behaves_like 'returns public, does not return hidden or confidential'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
-
it 'filters by confidentiality' do
expect(subject.to_sql).to match("issues.confidential")
end
@@ -1421,14 +1328,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
it_behaves_like 'returns public, does not return hidden or confidential'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
-
it 'filters by confidentiality' do
expect(subject.to_sql).to match("issues.confidential")
end
@@ -1439,14 +1338,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
it_behaves_like 'returns public and confidential, does not return hidden'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public, confidential, and hidden'
- end
-
it 'does not filter by confidentiality' do
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
@@ -1462,14 +1353,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
context 'when admin mode is enabled', :enable_admin_mode do
it_behaves_like 'returns public, confidential, and hidden'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public, confidential, and hidden'
- end
-
it 'does not filter by confidentiality' do
expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
@@ -1480,14 +1363,6 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
context 'when admin mode is disabled' do
it_behaves_like 'returns public, does not return hidden or confidential'
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it_behaves_like 'returns public and hidden, does not return confidential'
- end
-
it 'filters by confidentiality' do
expect(subject.to_sql).to match("issues.confidential")
end
diff --git a/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb
index 2700d29bf0e..5e7a2b77797 100644
--- a/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze|
- let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') }
+ let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) }
let_it_be(:container) { distribution_with_suite.container }
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container ) }
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) }
@@ -35,7 +35,7 @@ RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze|
context 'by suite' do
context 'with existing suite' do
- let(:params) { { suite: 'mysuite' } }
+ let(:params) { { suite: distribution_with_suite.suite } }
it 'finds distribution by suite' do
is_expected.to contain_exactly(distribution_with_suite)
@@ -61,7 +61,7 @@ RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze|
end
context 'with existing suite' do
- let(:params) { { codename_or_suite: 'mysuite' } }
+ let(:params) { { codename_or_suite: distribution_with_suite.suite } }
it 'finds distribution by suite' do
is_expected.to contain_exactly(distribution_with_suite)
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 f672ec7f5ac..2ec48aa405b 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
@@ -31,4 +31,88 @@ RSpec.shared_examples 'update work item description widget' do
expect(mutation_response['errors']).to match_array(['Description error message'])
end
end
+
+ context 'when the edited description includes quick action(s)' do
+ let(:input) { { 'descriptionWidget' => { 'description' => new_description } } }
+
+ shared_examples 'quick action is applied' do
+ before do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+
+ it 'applies the quick action(s)' do
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']).to include(expected_response)
+ end
+ end
+
+ context 'with /title quick action' do
+ it_behaves_like 'quick action is applied' do
+ let(:new_description) { "updated description\n/title updated title" }
+ let(:filtered_description) { "updated description" }
+
+ let(:expected_response) do
+ {
+ 'title' => 'updated title',
+ 'widgets' => include({
+ 'description' => filtered_description,
+ 'type' => 'DESCRIPTION'
+ })
+ }
+ end
+ end
+ end
+
+ context 'with /shrug, /tableflip and /cc quick action' do
+ it_behaves_like 'quick action is applied' do
+ let(:new_description) { "/tableflip updated description\n/shrug\n/cc @#{developer.username}" }
+ # note: \cc performs no action since 15.0
+ let(:filtered_description) { "updated description (╯°□°)╯︵ ┻━┻\n ¯\\_(ツ)_/¯\n/cc @#{developer.username}" }
+ let(:expected_response) do
+ {
+ 'widgets' => include({
+ 'description' => filtered_description,
+ 'type' => 'DESCRIPTION'
+ })
+ }
+ end
+ end
+ end
+
+ context 'with /close' do
+ it_behaves_like 'quick action is applied' do
+ let(:new_description) { "Resolved work item.\n/close" }
+ let(:filtered_description) { "Resolved work item." }
+ let(:expected_response) do
+ {
+ 'state' => 'CLOSED',
+ 'widgets' => include({
+ 'description' => filtered_description,
+ 'type' => 'DESCRIPTION'
+ })
+ }
+ end
+ end
+ end
+
+ context 'with /reopen' do
+ before do
+ work_item.close!
+ end
+
+ it_behaves_like 'quick action is applied' do
+ let(:new_description) { "Re-opening this work item.\n/reopen" }
+ let(:filtered_description) { "Re-opening this work item." }
+ let(:expected_response) do
+ {
+ 'state' => 'OPEN',
+ 'widgets' => include({
+ 'description' => filtered_description,
+ 'type' => 'DESCRIPTION'
+ })
+ }
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
index 16c2ab07f3a..d9a9400cb4e 100644
--- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-
+# TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418
RSpec.shared_examples 'no project services' do
it 'returns empty collection' do
expect(resolve_services).to be_empty
diff --git a/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb
index 25008bca619..6b2b45d28e8 100644
--- a/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/issuable_resolvers_shared_examples.rb
@@ -58,41 +58,6 @@ RSpec.shared_examples 'graphql query for searching issuables' do
end
end
- context 'with anonymous user' do
- let_it_be(:current_user) { nil }
-
- context 'with disable_anonymous_search as `true`' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it 'returns an error' do
- error_message = "User must be authenticated to include the `search` argument."
-
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
- resolve_issuables(search: 'created')
- end
- end
-
- it 'does not return error if search term is not present' do
- expect(resolve_issuables).not_to be_instance_of(Gitlab::Graphql::Errors::ArgumentError)
- end
- end
-
- context 'with disable_anonymous_search as `false`' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- parent.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it 'filters issuables by search term' do
- issuables = resolve_issuables(search: 'created')
-
- expect(issuables).to contain_exactly(issuable1, issuable2)
- end
- end
- end
-
def resolve_issuables(args = {}, obj = parent, context = { current_user: current_user })
resolve(described_class, obj: obj, args: args, ctx: context, arg_style: :internal)
end
diff --git a/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb
new file mode 100644
index 00000000000..0e09a9d9e66
--- /dev/null
+++ b/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'releases and group releases resolver' do
+ context 'when the user does not have access to the project' do
+ let(:current_user) { public_user }
+
+ it 'returns an empty array' do
+ expect(resolve_releases).to be_empty
+ end
+ end
+
+ context "when the user has full access to the project's releases" do
+ let(:current_user) { developer }
+
+ it 'returns all releases associated to the project' do
+ expect(resolve_releases).to match_array(all_releases)
+ end
+
+ describe 'when order_by is released_at' do
+ context 'with sort: desc' do
+ let(:args) { { sort: :released_at_desc } }
+
+ it 'returns the releases ordered by released_at in descending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:released_at, :desc)
+ end
+ end
+
+ context 'with sort: asc' do
+ let(:args) { { sort: :released_at_asc } }
+
+ it 'returns the releases ordered by released_at in ascending order' do
+ expect(resolve_releases.to_a)
+ .to match_array(all_releases)
+ .and be_sorted(:released_at, :asc)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb b/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb
new file mode 100644
index 00000000000..949eb4fb643
--- /dev/null
+++ b/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'graphql notes subscriptions' do
+ describe '#resolve' do
+ let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:work_item) { create(:work_item, :task) }
+ let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) }
+ let_it_be(:current_user) { work_item.author }
+ let_it_be(:noteable_id) { work_item.to_gid }
+
+ subject { resolver.resolve_with_support(noteable_id: noteable_id) }
+
+ context 'on initial subscription' do
+ let(:resolver) do
+ resolver_instance(described_class, ctx: { current_user: current_user }, subscription_update: false)
+ end
+
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when user is unauthorized' do
+ let(:current_user) { unauthorized_user }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(GraphQL::ExecutionError)
+ end
+ end
+
+ context 'when work_item does not exist' do
+ let(:noteable_id) { GlobalID.parse("gid://gitlab/WorkItem/#{non_existing_record_id}") }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(GraphQL::ExecutionError)
+ end
+ end
+ end
+
+ context 'on subscription updates' do
+ let(:resolver) do
+ resolver_instance(described_class, obj: note, ctx: { current_user: current_user }, subscription_update: true)
+ end
+
+ it 'returns the resolved object' do
+ expect(subject).to eq(note)
+ end
+
+ context 'when user is unauthorized' do
+ let(:current_user) { unauthorized_user }
+
+ it 'unsubscribes the user' do
+ # GraphQL::Execution::Execute::Skip is returned when unsubscribed
+ expect(subject).to be_an(GraphQL::Execution::Execute::Skip)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index 67576a18c80..4dc2ce61c4d 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -5,8 +5,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'raises an informative error if `deprecation_reason` is used' do
expect { subject(deprecation_reason: 'foo') }.to raise_error(
ArgumentError,
- 'Use `deprecated` property instead of `deprecation_reason`. ' \
- 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items'
+ start_with('Use `deprecated` property instead of `deprecation_reason`.')
)
end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index 19ceb465383..bb33a7559dc 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -41,6 +41,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do
preferencesGitpodPath
profileEnableGitpodPath
savedReplies
+ savedReply
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index 5041ac4a660..aeb4e0feb12 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -4,9 +4,10 @@ RSpec.shared_examples 'integration settings form' do
include IntegrationsHelper
# Note: these specs don't validate channel fields
# which are present on a few integrations
- it 'displays all the integrations' do
+ it 'displays all the integrations', feature_category: :integrations do
aggregate_failures do
integrations.each do |integration|
+ stub_feature_flags(integration_slack_app_notifications: false)
navigate_to_integration(integration)
page.within('form.integration-settings-form') do
@@ -53,12 +54,14 @@ RSpec.shared_examples 'integration settings form' do
# Should match `integrationTriggerEventTitles` in app/assets/javascripts/integrations/constants.js
event_titles = {
push_events: s_('IntegrationEvents|A push is made to the repository'),
- issues_events: s_('IntegrationEvents|IntegrationEvents|An issue is created, updated, or closed'),
- confidential_issues_events: s_('IntegrationEvents|A confidential issue is created, updated, or closed'),
- merge_requests_events: s_('IntegrationEvents|A merge request is created, updated, or merged'),
- note_events: s_('IntegrationEvents|A comment is added on an issue'),
- confidential_note_events: s_('IntegrationEvents|A comment is added on a confidential issue'),
- tag_push_events: s_('IntegrationEvents|A tag is pushed to the repository'),
+ issues_events: s_('IntegrationEvents|An issue is created, closed, or reopened'),
+ confidential_issues_events: s_('A confidential issue is created, closed, or reopened'),
+ merge_requests_events: s_('IntegrationEvents|A merge request is created, merged, closed, or reopened'),
+ note_events: s_('IntegrationEvents|A comment is added'),
+ confidential_note_events: s_(
+ 'IntegrationEvents|An internal note or comment on a confidential issue is added'
+ ),
+ tag_push_events: s_('IntegrationEvents|A tag is pushed to the repository or removed'),
pipeline_events: s_('IntegrationEvents|A pipeline status changes'),
wiki_page_events: s_('IntegrationEvents|A wiki page is created or updated')
}.with_indifferent_access
diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
index 6cdd7954b5f..42e8fa3d51f 100644
--- a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_examples_for 'object cache helper' do
end
it "fetches from the cache" do
- expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once
+ expect(instance.cache).to receive(:fetch).with("#{expected_cache_key_prefix}:#{presentable.cache_key}:#{user.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once
subject
end
@@ -28,7 +28,7 @@ RSpec.shared_examples_for 'object cache helper' do
end
it "uses the context to augment the cache key" do
- expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{project.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once
+ expect(instance.cache).to receive(:fetch).with("#{expected_cache_key_prefix}:#{presentable.cache_key}:#{project.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once
subject
end
@@ -38,7 +38,7 @@ RSpec.shared_examples_for 'object cache helper' do
it "sets the expiry when accessing the cache" do
kwargs[:expires_in] = 7.days
- expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: 7.days).once
+ expect(instance.cache).to receive(:fetch).with("#{expected_cache_key_prefix}:#{presentable.cache_key}:#{user.cache_key}", expires_in: 7.days).once
subject
end
@@ -90,7 +90,7 @@ RSpec.shared_examples_for 'collection cache helper' do
end
it "fetches from the cache" do
- keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" }
+ keys = presentable.map { |item| "#{expected_cache_key_prefix}:#{item.cache_key}:#{user.cache_key}" }
expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original
@@ -103,7 +103,7 @@ RSpec.shared_examples_for 'collection cache helper' do
end
it "uses the context to augment the cache key" do
- keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{project.cache_key}" }
+ keys = presentable.map { |item| "#{expected_cache_key_prefix}:#{item.cache_key}:#{project.cache_key}" }
expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original
@@ -113,7 +113,7 @@ RSpec.shared_examples_for 'collection cache helper' do
context "expires_in is supplied" do
it "sets the expiry when accessing the cache" do
- keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" }
+ keys = presentable.map { |item| "#{expected_cache_key_prefix}:#{item.cache_key}:#{user.cache_key}" }
kwargs[:expires_in] = 7.days
expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: 7.days).once.and_call_original
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index b0cbf0b0d65..2e182fb399d 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -280,6 +280,12 @@ RSpec.shared_examples 'no email is sent' do
end
end
+RSpec.shared_examples 'a mail with default delivery method' do
+ it 'uses mailer default delivery method' do
+ expect(subject.delivery_method).to be_a ActionMailer::Base.delivery_methods[described_class.delivery_method]
+ end
+end
+
RSpec.shared_examples 'does not render a manage notifications link' do
it do
aggregate_failures do
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index 6d0462a9ee8..085fec6ff1e 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
describe "#execute" do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
let(:webhook_url) { "https://example.gitlab.com/" }
let(:webhook_url_regex) { /\A#{webhook_url}.*/ }
@@ -165,7 +165,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with issue events" do
let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do
- service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
+ service = Issues::CreateService.new(container: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute[:issue]
service.hook_data(issue, "open")
end
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
new file mode 100644
index 00000000000..122774a9028
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
+ shared_examples 'is tolerant of invalid records' do
+ specify do
+ hook.url = nil
+
+ expect(hook).to be_invalid
+ run_expectation
+ end
+ end
+
+ describe '.executable/.disabled', :freeze_time do
+ let!(:not_executable) do
+ [
+ [4, nil], # Exceeded the grace period, set by #fail!
+ [4, 1.second.from_now], # Exceeded the grace period, set by #backoff!
+ [4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ let!(:executables) do
+ expired = 1.second.ago
+ borderline = Time.current
+ suspended = 1.second.from_now
+
+ [
+ # Most of these are impossible states, but are included for completeness
+ [0, nil],
+ [1, nil],
+ [3, nil],
+ [4, expired],
+
+ # Impossible cases:
+ [3, suspended],
+ [3, expired],
+ [3, borderline],
+ [1, suspended],
+ [1, expired],
+ [1, borderline],
+ [0, borderline],
+ [0, suspended],
+ [0, expired]
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.executable).to all(be_executable)
+
+ # As expected, and consistent
+ expect(find_hooks.disabled).to match_array not_executable
+ expect(find_hooks.disabled.map(&:executable?)).not_to include(true)
+
+ # Nothing is missing
+ expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
+ end
+ end
+
+ describe '#executable?', :freeze_time do
+ let(:web_hook) { create(hook_factory, **default_factory_arguments) }
+
+ where(:recent_failures, :not_until, :executable) do
+ [
+ [0, :not_set, true],
+ [0, :past, true],
+ [0, :future, true],
+ [0, :now, true],
+ [1, :not_set, true],
+ [1, :past, true],
+ [1, :future, true],
+ [3, :not_set, true],
+ [3, :past, true],
+ [3, :future, true],
+ [4, :not_set, false],
+ [4, :past, true], # expired suspension
+ [4, :now, false], # active suspension
+ [4, :future, false] # active suspension
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook.executable?).to eq(executable)
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable if it was marked as failed' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'makes a hook executable if it is currently backed off' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'does not update hooks unless necessary' do
+ hook
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+ end
+ end
+
+ describe '#backoff!' do
+ context 'when we have not backed off before' do
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ context 'when we have exhausted the grace period' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ end
+
+ context 'when the hook is permanently disabled' do
+ before do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+ end
+
+ it 'does not set disabled_until' do
+ expect { hook.backoff! }.not_to change { hook.disabled_until }
+ end
+
+ it 'does not increment the backoff count' do
+ expect { hook.backoff! }.not_to change { hook.backoff_count }
+ end
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe '#failed!' do
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.failed! }.to change { hook.recent_failures }.by(1)
+ end
+ end
+ end
+
+ describe '#disable!' do
+ it 'disables a hook' do
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+
+ it 'does nothing if the hook is already disabled' do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.disable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#temporarily_disabled?' do
+ it 'is false when not temporarily disabled' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
+ WebHook::FAILURE_THRESHOLD.times do
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ hook.backoff!
+ expect(hook).to be_temporarily_disabled
+ end
+
+ context 'when hook has been told to back off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.backoff!
+ end
+
+ it 'is true' do
+ expect(hook).to be_temporarily_disabled
+ end
+ end
+ end
+
+ describe '#permanently_disabled?' do
+ it 'is false when not disabled' do
+ expect(hook).not_to be_permanently_disabled
+ end
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it 'is true' do
+ expect(hook).to be_permanently_disabled
+ end
+ end
+ end
+
+ describe '#alert_status' do
+ subject(:status) { hook.alert_status }
+
+ it { is_expected.to eq :executable }
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it { is_expected.to eq :disabled }
+ end
+
+ context 'when hook has been backed off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.disabled_until = 1.hour.from_now
+ end
+
+ it { is_expected.to eq :temporarily_disabled }
+ end
+ end
+end
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 f98be12523d..5755b9a56b1 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
@@ -15,7 +15,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
describe attribute do
describe '#increment_counter', :redis do
let(:amount) { 10 }
- let(:increment) { Gitlab::Counters::Increment.new(amount: amount) }
+ let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) }
let(:counter_key) { model.counter(attribute).key }
subject { model.increment_counter(attribute, increment) }
@@ -31,6 +31,7 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
attribute: attribute,
project_id: model.project_id,
increment: amount,
+ ref: increment.ref,
new_counter_value: 0 + amount,
current_db_value: model.read_attribute(attribute),
'correlation_id' => an_instance_of(String),
@@ -74,27 +75,36 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
describe '#bulk_increment_counter', :redis do
- let(:increments) { [Gitlab::Counters::Increment.new(amount: 10), Gitlab::Counters::Increment.new(amount: 5)] }
+ let(:increments) do
+ [
+ Gitlab::Counters::Increment.new(amount: 10, ref: 1),
+ Gitlab::Counters::Increment.new(amount: 5, ref: 2)
+ ]
+ end
+
let(:total_amount) { increments.sum(&:amount) }
let(:counter_key) { model.counter(attribute).key }
subject { model.bulk_increment_counter(attribute, increments) }
context 'when attribute is a counter attribute' do
- it 'increments the counter in Redis and logs it' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Increment counter attribute',
- attribute: attribute,
- project_id: model.project_id,
- increment: total_amount,
- new_counter_value: 0 + total_amount,
- current_db_value: model.read_attribute(attribute),
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
+ it 'increments the counter in Redis and logs each increment' do
+ increments.each do |increment|
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Increment counter attribute',
+ attribute: attribute,
+ project_id: model.project_id,
+ increment: increment.amount,
+ ref: increment.ref,
+ new_counter_value: 0 + total_amount,
+ current_db_value: model.read_attribute(attribute),
+ 'correlation_id' => an_instance_of(String),
+ 'meta.feature_category' => 'test',
+ 'meta.caller_id' => 'caller'
+ )
)
- )
+ end
subject
@@ -104,6 +114,30 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
+ context 'when feature flag split_log_bulk_increment_counter is disabled' do
+ before do
+ stub_feature_flags(split_log_bulk_increment_counter: false)
+ end
+
+ it 'logs a single total increment' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ hash_including(
+ message: 'Increment counter attribute',
+ attribute: attribute,
+ project_id: model.project_id,
+ increment: increments.sum(&:amount),
+ new_counter_value: 0 + total_amount,
+ current_db_value: model.read_attribute(attribute),
+ 'correlation_id' => an_instance_of(String),
+ 'meta.feature_category' => 'test',
+ 'meta.caller_id' => 'caller'
+ )
+ )
+
+ subject
+ end
+ end
+
it 'does not increment the counter for the record' do
expect { subject }.not_to change { model.reset.read_attribute(attribute) }
end
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index 974fc8f402a..0ef9ab25505 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -276,7 +276,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
describe 'Push events' do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
+ let_it_be_with_refind(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_integration).to receive_messages(
@@ -520,7 +520,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
describe 'Pipeline events' do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
+ let_it_be_with_refind(:project) { create(:project, :repository, creator: user) }
let(:pipeline) do
create(:ci_pipeline,
project: project, status: status,
@@ -671,7 +671,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
describe 'Deployment events' do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
+ let_it_be_with_refind(:project) { create(:project, :repository, creator: user) }
let_it_be(:deployment) do
create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch)
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
new file mode 100644
index 00000000000..848840ee297
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a hook that does not get automatically disabled on failure' do
+ describe '.executable/.disabled', :freeze_time do
+ let!(:executables) 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(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(find_hooks).to all(be_executable)
+ expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.disabled).to be_empty
+ end
+ end
+
+ describe '#executable?', :freeze_time do
+ let(:web_hook) { create(hook_factory, **default_factory_arguments) }
+
+ where(:recent_failures, :not_until) do
+ [
+ [0, :not_set],
+ [0, :past],
+ [0, :future],
+ [0, :now],
+ [1, :not_set],
+ [1, :past],
+ [1, :future],
+ [3, :not_set],
+ [3, :past],
+ [3, :future],
+ [4, :not_set],
+ [4, :past], # expired suspension
+ [4, :now], # active suspension
+ [4, :future] # active suspension
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook).to be_executable
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable if it was marked as failed' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.not_to change { hook.executable? }.from(true)
+ end
+
+ it 'makes a hook executable if it is currently backed off' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect { hook.enable! }.not_to change { hook.executable? }.from(true)
+ end
+
+ it 'does not update hooks unless necessary' do
+ hook
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+ end
+
+ describe '#backoff!' do
+ context 'when we have not backed off before' do
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ context 'when we have exhausted the grace period' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ end
+
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+ end
+
+ describe '#disable!' do
+ it 'does not disable a group hook' do
+ expect { hook.disable! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ describe '#temporarily_disabled?' do
+ it 'is false' do
+ # Initially
+ expect(hook).not_to be_temporarily_disabled
+
+ # Backing off
+ WebHook::FAILURE_THRESHOLD.times do
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+ end
+
+ describe '#permanently_disabled?' do
+ it 'is false' do
+ # Initially
+ expect(hook).not_to be_permanently_disabled
+
+ hook.disable!
+
+ expect(hook).not_to be_permanently_disabled
+ end
+ end
+
+ describe '#alert_status' do
+ subject(:status) { hook.alert_status }
+
+ it { is_expected.to eq :executable }
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it { is_expected.to eq :executable }
+ end
+
+ context 'when hook has been backed off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.disabled_until = 1.hour.from_now
+ end
+
+ it { is_expected.to eq :executable }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
new file mode 100644
index 00000000000..cd6eb8c77fa
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'something that has web-hooks' do
+ describe '#any_hook_failed?', :clean_gitlab_redis_shared_state do
+ subject { object.any_hook_failed? }
+
+ context 'when there are no hooks' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when there are hooks' do
+ before do
+ create_hook
+ create_hook
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'when there is a failed hook' do
+ before do
+ hook = create_hook
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
+
+ describe '#cache_web_hook_failure', :clean_gitlab_redis_shared_state do
+ context 'when no value is passed' do
+ it 'stores the return value of #any_hook_failed? and passes it back' do
+ allow(object).to receive(:any_hook_failed?).and_return(true)
+
+ Gitlab::Redis::SharedState.with do |r|
+ expect(r).to receive(:set)
+ .with(object.web_hook_failure_redis_key, 'true', ex: 1.hour)
+ .and_call_original
+ end
+
+ expect(object.cache_web_hook_failure).to eq(true)
+ end
+ end
+
+ context 'when a value is passed' do
+ it 'stores the value and passes it back' do
+ expect(object).not_to receive(:any_hook_failed?)
+
+ Gitlab::Redis::SharedState.with do |r|
+ expect(r).to receive(:set)
+ .with(object.web_hook_failure_redis_key, 'foo', ex: 1.hour)
+ .and_call_original
+ end
+
+ expect(object.cache_web_hook_failure(:foo)).to eq(:foo)
+ end
+ end
+ end
+
+ describe '#get_web_hook_failure', :clean_gitlab_redis_shared_state do
+ subject { object.get_web_hook_failure }
+
+ context 'when no value is stored' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when stored as true' do
+ before do
+ object.cache_web_hook_failure(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when stored as false' do
+ before do
+ object.cache_web_hook_failure(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do
+ context 'when a value has not been stored' do
+ it 'does not call #any_hook_failed?' do
+ expect(object.get_web_hook_failure).to be_nil
+ expect(object).to receive(:any_hook_failed?).and_return(true)
+
+ expect(object.fetch_web_hook_failure).to eq(true)
+ expect(object.get_web_hook_failure).to eq(true)
+ end
+ end
+
+ context 'when a value has been stored' do
+ before do
+ object.cache_web_hook_failure(true)
+ end
+
+ it 'does not call #any_hook_failed?' do
+ expect(object).not_to receive(:any_hook_failed?)
+
+ expect(object.fetch_web_hook_failure).to eq(true)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/exportable_shared_examples.rb b/spec/support/shared_examples/models/exportable_shared_examples.rb
new file mode 100644
index 00000000000..37c3e68fd5f
--- /dev/null
+++ b/spec/support/shared_examples/models/exportable_shared_examples.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource with exportable associations' do
+ before do
+ stub_licensed_features(stubbed_features) if stubbed_features.any?
+ end
+
+ describe '#exportable_association?' do
+ let(:association) { single_association }
+
+ subject { resource.exportable_association?(association, current_user: user) }
+
+ it { is_expected.to be_falsey }
+
+ context 'when user can read resource' do
+ before do
+ group.add_developer(user)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context "when user can read resource's association" do
+ before do
+ other_group.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'for an unknown association' do
+ let(:association) { :foo }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for an unauthenticated user' do
+ let(:user) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+ end
+
+ describe '#readable_records' do
+ subject { resource.readable_records(association, current_user: user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when association not supported' do
+ let(:association) { :foo }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when association is `:notes`' do
+ let(:association) { :notes }
+
+ it { is_expected.to match_array([readable_note]) }
+
+ context 'when user have access' do
+ before do
+ other_group.add_developer(user)
+ end
+
+ it 'returns all records' do
+ is_expected.to match_array([readable_note, restricted_note])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 3d7d97bbeae..ac4ad4525aa 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
- let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') }
+ let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) }
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: distribution_with_suite.container ) }
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) }
let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) }
diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb
index 8cab2de076d..038ff33c68a 100644
--- a/spec/support/shared_examples/models/resource_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb
@@ -160,6 +160,16 @@ RSpec.shared_examples 'a resource event for merge requests' do
end
end
end
+
+ context 'on callbacks' do
+ it 'does not trigger note created subscription' do
+ event = build(described_class.name.underscore.to_sym, merge_request: merge_request1)
+
+ expect(GraphqlTriggers).not_to receive(:work_item_note_created)
+ expect(event).not_to receive(:trigger_note_subscription_create)
+ event.save!
+ end
+ end
end
RSpec.shared_examples 'a note for work item resource event' do
@@ -172,4 +182,14 @@ RSpec.shared_examples 'a note for work item resource event' do
expect(event.work_item_synthetic_system_note.class.name).to eq(event.synthetic_note_class.name)
end
+
+ context 'on callbacks' do
+ it 'triggers note created subscription' do
+ event = build(described_class.name.underscore.to_sym, issue: work_item)
+
+ expect(GraphqlTriggers).to receive(:work_item_note_created)
+ expect(event).to receive(:trigger_note_subscription_create).and_call_original
+ event.save!
+ end
+ end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index 73e22b97abc..600539f7d0a 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -57,11 +57,6 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#ancestors' do
- before do
- # #reload is called to make sure traversal_ids are reloaded
- reload_models(group, nested_group, deep_nested_group, very_deep_nested_group)
- end
-
it 'returns the correct ancestors' do
expect(very_deep_nested_group.ancestors).to contain_exactly(group, nested_group, deep_nested_group)
expect(deep_nested_group.ancestors).to contain_exactly(group, nested_group)
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 15d56c402d1..9ec1b8b3f5a 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -333,6 +333,7 @@ RSpec.shared_examples 'project policies as admin with admin mode' do
expect_disallowed(*team_member_reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*maintainer_permissions)
+ expect_allowed(*admin_permissions)
expect_allowed(*owner_permissions)
end
@@ -354,6 +355,7 @@ RSpec.shared_examples 'project policies as admin with admin mode' do
expect_disallowed(*team_member_reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*admin_permissions)
expect_disallowed(*owner_permissions)
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
index f5431b29ee2..b5704ad8f17 100644
--- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
@@ -54,43 +54,3 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do
end
end
end
-
-RSpec.shared_examples 'does not exceed the issuable size limit with ff off' do
- let(:user1) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
-
- before do
- project.add_maintainer(user)
- project.add_maintainer(user1)
- project.add_maintainer(user2)
- project.add_maintainer(user3)
- end
-
- context 'when feature flag is off' do
- before do
- stub_feature_flags(feature_flag_hash)
- end
-
- context "when the number of users of issuable does exceed the limit" do
- before do
- stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
- end
-
- it 'will not add more than the allowed number of users' do
- allow_next_instance_of(MergeRequests::UpdateService) do |service|
- expect(service).to receive(:execute).and_call_original
- end
-
- note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: 'MergeRequest',
- noteable_id: issuable.id,
- confidential: false
- )).execute
-
- expect(note.errors[:validation]).to be_empty
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
new file mode 100644
index 00000000000..07fde7d3f35
--- /dev/null
+++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+RSpec.shared_examples 'GET request permissions for admin mode' do
+ it_behaves_like 'GET request permissions for admin mode when user'
+ it_behaves_like 'GET request permissions for admin mode when admin'
+end
+
+RSpec.shared_examples 'PUT request permissions for admin mode' do |params|
+ it_behaves_like 'PUT request permissions for admin mode when user', params
+ it_behaves_like 'PUT request permissions for admin mode when admin', params
+end
+
+RSpec.shared_examples 'POST request permissions for admin mode' do |params|
+ it_behaves_like 'POST request permissions for admin mode when user', params
+ it_behaves_like 'POST request permissions for admin mode when admin', params
+end
+
+RSpec.shared_examples 'DELETE request permissions for admin mode' do
+ it_behaves_like 'DELETE request permissions for admin mode when user'
+ it_behaves_like 'DELETE request permissions for admin mode when admin'
+end
+
+RSpec.shared_examples 'GET request permissions for admin mode when user' do
+ subject { get api(path, current_user, admin_mode: admin_mode) }
+
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'admin mode on', true, :forbidden
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'GET request permissions for admin mode when admin' do
+ subject { get api(path, current_user, admin_mode: admin_mode) }
+
+ let_it_be(:current_user) { create(:admin) }
+
+ it_behaves_like 'admin mode on', true, :ok
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params|
+ subject { put api(path, current_user, admin_mode: admin_mode), params: params }
+
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'admin mode on', true, :forbidden
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params|
+ subject { put api(path, current_user, admin_mode: admin_mode), params: params }
+
+ let_it_be(:current_user) { create(:admin) }
+
+ it_behaves_like 'admin mode on', true, :ok
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'POST request permissions for admin mode when user' do |params|
+ subject { post api(path, current_user, admin_mode: admin_mode), params: params }
+
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'admin mode on', true, :forbidden
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params|
+ subject { post api(path, current_user, admin_mode: admin_mode), params: params }
+
+ let_it_be(:current_user) { create(:admin) }
+
+ it_behaves_like 'admin mode on', true, :created
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'DELETE request permissions for admin mode when user' do
+ subject { delete api(path, current_user, admin_mode: admin_mode) }
+
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'admin mode on', true, :forbidden
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do
+ subject { delete api(path, current_user, admin_mode: admin_mode) }
+
+ let_it_be(:current_user) { create(:admin) }
+
+ it_behaves_like 'admin mode on', true, :no_content
+ it_behaves_like 'admin mode on', false, :forbidden
+end
+
+RSpec.shared_examples "admin mode on" do |admin_mode, status|
+ let_it_be(:admin_mode) { admin_mode }
+
+ it_behaves_like 'returning response status', status
+end
diff --git a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb
index 2ba42b8e8fa..e0225070986 100644
--- a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb
@@ -15,9 +15,3 @@ RSpec.shared_examples 'rejects Debian access with unknown container id' do |anon
end
end
end
-
-RSpec.shared_examples 'Debian API FIPS mode' do
- context 'when FIPS mode is enabled', :fips_mode do
- it_behaves_like 'returning response status', :not_found
- end
-end
diff --git a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb
index f13ac05591c..5cd63c33936 100644
--- a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb
@@ -3,8 +3,6 @@
RSpec.shared_examples 'Debian distributions GET request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
- it_behaves_like 'Debian API FIPS mode'
-
it "returns #{status}#{and_body}" do
subject
@@ -19,8 +17,6 @@ end
RSpec.shared_examples 'Debian distributions PUT request' do |status, body|
and_body = body.nil? ? '' : ' and expected body'
- it_behaves_like 'Debian API FIPS mode'
-
if status == :success
it 'updates distribution', :aggregate_failures do
expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original
@@ -53,8 +49,6 @@ end
RSpec.shared_examples 'Debian distributions DELETE request' do |status, body|
and_body = body.nil? ? '' : ' and expected body'
- it_behaves_like 'Debian API FIPS mode'
-
if status == :success
it 'updates distribution', :aggregate_failures do
expect { subject }
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 14a83d2889b..6d29076da0f 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -3,8 +3,6 @@
RSpec.shared_examples 'Debian packages GET request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
- it_behaves_like 'Debian API FIPS mode'
-
it "returns #{status}#{and_body}" do
subject
@@ -19,11 +17,8 @@ end
RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
and_body = body.nil? ? '' : ' and expected body'
- it_behaves_like 'Debian API FIPS mode'
-
if status == :created
it 'creates package files', :aggregate_failures do
- expect(::Packages::Debian::FindOrCreateIncomingService).to receive(:new).with(container, user).and_call_original
expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(package: be_a(Packages::Package), current_user: be_an(User), params: be_an(Hash)).and_call_original
if file_name.end_with? '.changes'
@@ -32,10 +27,23 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
end
- expect { subject }
+ if extra_params[:distribution]
+ expect(::Packages::Debian::FindOrCreateIncomingService).not_to receive(:new)
+ expect(::Packages::Debian::ProcessPackageFileWorker).to receive(:perform_async)
+
+ expect { subject }
+ .to change { container.packages.debian.count }.by(1)
+ .and not_change { container.packages.debian.where(name: 'incoming').count }
+ .and change { container.package_files.count }.by(1)
+ else
+ expect(::Packages::Debian::FindOrCreateIncomingService).to receive(:new).with(container, user).and_call_original
+ expect(::Packages::Debian::ProcessPackageFileWorker).not_to receive(:perform_async)
+
+ expect { subject }
.to change { container.packages.debian.count }.by(1)
.and change { container.packages.debian.where(name: 'incoming').count }.by(1)
.and change { container.package_files.count }.by(1)
+ end
expect(response).to have_gitlab_http_status(status)
expect(response.media_type).to eq('text/plain')
diff --git a/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb
new file mode 100644
index 00000000000..306310c9e9c
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Requires `current_user`, `data_path`, `variables`, and `pagination_query(params)` bindings
+RSpec.shared_examples 'sorted paginated variables' do
+ subject(:expected_ordered_variables) { ordered_variables.map { |var| var.to_global_id.to_s } }
+
+ context 'when sorted by key ascending' do
+ let(:ordered_variables) { variables.sort_by(&:key) }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :KEY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { expected_ordered_variables }
+ end
+ end
+
+ context 'when sorted by key descending' do
+ let(:ordered_variables) { variables.sort_by(&:key).reverse }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :KEY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { expected_ordered_variables }
+ end
+ 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 d4af9e570d1..6c8b792bf92 100644
--- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'graphql issue list request spec' do
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('issues'.classify)}
+ #{all_graphql_fields_for('issues'.classify, excluded: ['relatedMergeRequests'])}
}
QUERY
end
@@ -683,6 +683,28 @@ RSpec.shared_examples 'graphql issue list request spec' do
end
end
+ context 'when selecting `related_merge_requests`' do
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ relatedMergeRequests {
+ nodes {
+ id
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'limits the field to 1 execution' do
+ post_query
+
+ expect_graphql_errors_to_include(
+ '"relatedMergeRequests" field can be requested only for 1 Issue(s) at a time.'
+ )
+ end
+ end
+
it 'includes a web_url' do
post_query
diff --git a/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
new file mode 100644
index 00000000000..b40cf6daea9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'correct total count' do
+ let(:data) { graphql_data.dig(resource_type.to_s, 'releases') }
+
+ before do
+ create_list(:release, 2, project: project)
+
+ post_query
+ end
+
+ it 'returns the total count' do
+ expect(data['count']).to eq(project.releases.count)
+ end
+end
+
+RSpec.shared_examples 'full access to all repository-related fields' do
+ describe 'repository-related fields' do
+ before do
+ post_query
+ end
+
+ it 'returns data for fields that are protected in private projects' do
+ expected_sources = release.sources.map do |s|
+ { 'url' => s.url }
+ end
+
+ expected_evidences = release.evidences.map do |e|
+ { 'sha' => e.sha }
+ end
+ expect(data).to eq(
+ 'tagName' => release.tag,
+ 'tagPath' => project_tag_path(project, release.tag),
+ 'name' => release.name,
+ 'commit' => {
+ 'sha' => release.commit.sha
+ },
+ 'assets' => {
+ 'count' => release.assets_count,
+ 'sources' => {
+ 'nodes' => expected_sources
+ }
+ },
+ 'evidences' => {
+ 'nodes' => expected_evidences
+ },
+ 'links' => {
+ 'selfUrl' => project_release_url(project, release),
+ 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
+ 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
+ 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
+ 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
+ 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
+ }
+ )
+ end
+ end
+
+ it_behaves_like 'correct total count'
+end
+
+RSpec.shared_examples 'no access to any repository-related fields' do
+ describe 'repository-related fields' do
+ before do
+ post_query
+ end
+
+ it 'does not return data for fields that expose repository information' do
+ tag_name = release.tag
+ release_name = release.name
+ expect(data).to eq(
+ 'tagName' => tag_name,
+ 'tagPath' => nil,
+ 'name' => release_name,
+ 'commit' => nil,
+ 'assets' => {
+ 'count' => release.assets_count(except: [:sources]),
+ 'sources' => {
+ 'nodes' => []
+ }
+ },
+ 'evidences' => {
+ 'nodes' => []
+ },
+ 'links' => {
+ 'closedIssuesUrl' => nil,
+ 'closedMergeRequestsUrl' => nil,
+ 'mergedMergeRequestsUrl' => nil,
+ 'openedIssuesUrl' => nil,
+ 'openedMergeRequestsUrl' => nil,
+ 'selfUrl' => project_release_url(project, release)
+ }
+ )
+ end
+ end
+
+ it_behaves_like 'correct total count'
+end
+
+RSpec.shared_examples 'access to editUrl' do
+ # editUrl is tested separately because its permissions
+ # are slightly different than other release fields
+ let(:query) do
+ graphql_query_for(resource_type, { fullPath: resource.full_path },
+ %(
+ releases {
+ nodes {
+ links {
+ editUrl
+ }
+ }
+ }
+ ))
+ end
+
+ before do
+ post_query
+ end
+
+ it 'returns editUrl' do
+ expect(data).to eq(
+ 'links' => {
+ 'editUrl' => edit_project_release_url(project, release)
+ }
+ )
+ end
+end
+
+RSpec.shared_examples 'no access to editUrl' do
+ let(:query) do
+ graphql_query_for(resource_type, { fullPath: resource.full_path },
+ %(
+ releases {
+ nodes {
+ links {
+ editUrl
+ }
+ }
+ }
+ ))
+ end
+
+ before do
+ post_query
+ end
+
+ it 'does not return editUrl' do
+ expect(data).to eq(
+ 'links' => {
+ 'editUrl' => nil
+ }
+ )
+ end
+end
+
+RSpec.shared_examples 'no access to any release data' do
+ before do
+ post_query
+ end
+
+ it 'returns nil' do
+ expect(data).to eq(nil)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
index d666a754d9f..f2002de4b55 100644
--- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
@@ -128,7 +128,8 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
get api(hook_uri, user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include('alert_status' => 'disabled')
+
+ expect(json_response).to include('alert_status' => 'disabled') unless hook.executable?
end
end
@@ -142,10 +143,13 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
get api(hook_uri, user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include(
- 'alert_status' => 'temporarily_disabled',
- 'disabled_until' => hook.disabled_until.iso8601(3)
- )
+
+ unless hook.executable?
+ expect(json_response).to include(
+ 'alert_status' => 'temporarily_disabled',
+ 'disabled_until' => hook.disabled_until.iso8601(3)
+ )
+ end
end
end
end
diff --git a/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb
index fcde3b65b4f..f06a80375e8 100644
--- a/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/issuable_search_shared_examples.rb
@@ -1,40 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'issuable anonymous search' do
- context 'with anonymous user' do
- context 'with disable_anonymous_search disabled' do
- before do
- stub_feature_flags(disable_anonymous_search: false)
- end
-
- it 'returns issuables matching given search string for title' do
- get api(url), params: { scope: 'all', search: issuable.title }
-
- expect_paginated_array_response(result)
- end
-
- it 'returns issuables matching given search string for description' do
- get api(url), params: { scope: 'all', search: issuable.description }
-
- expect_paginated_array_response(result)
- end
- end
-
- context 'with disable_anonymous_search enabled' do
- before do
- stub_feature_flags(disable_anonymous_search: true)
- end
-
- it "returns 422 error" do
- get api(url), params: { scope: 'all', search: issuable.title }
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to eq('User must be authenticated to use search')
- end
- end
- end
-end
-
RSpec.shared_examples 'issuable API rate-limited search' do
it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do
let(:current_user) { user }
@@ -49,20 +14,4 @@ RSpec.shared_examples 'issuable API rate-limited search' do
get api(url), params: { scope: 'all', search: issuable.title }
end
end
-
- context 'when rate_limit_issuable_searches is disabled', :freeze_time, :clean_gitlab_redis_rate_limiting do
- before do
- stub_feature_flags(rate_limit_issuable_searches: false)
-
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold)
- .with(:search_rate_limit_unauthenticated).and_return(1)
- end
-
- it 'does not enforce the rate limit' do
- get api(url), params: { scope: 'all', search: issuable.title }
- get api(url), params: { scope: 'all', search: issuable.title }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
end
diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
index 381583ff2a9..86a1fd76d09 100644
--- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
@@ -142,7 +142,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
if issuable_name == 'issue'
it 'calls update service without :use_specialized_service param' do
expect(::Issues::UpdateService).to receive(:new).with(
- project: project,
+ container: project,
current_user: user,
params: { spend_time: { duration: 3600, summary: 'summary', user_id: user.id } })
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 82ed6eb4c95..3f457890f35 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -382,7 +382,7 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do
end
def reset_rack_attack
- Rack::Attack.reset!
+ Gitlab::Redis::RateLimiting.with(&:flushdb)
Rack::Attack.clear_configuration
Gitlab::RackAttack.configure(Rack::Attack)
end
diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
index 2e557ca090c..b5e3a407b53 100644
--- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
@@ -38,6 +38,10 @@ RSpec.shared_examples 'note entity' do
expect(subject[:current_user]).to include(:can_edit, :can_award_emoji, :can_resolve, :can_resolve_discussion)
end
+ it 'exposes the report_abuse_path' do
+ expect(subject[:report_abuse_path]).to eq(add_category_abuse_reports_path)
+ end
+
describe ':can_resolve_discussion' do
context 'discussion is resolvable' do
before do
diff --git a/spec/support/shared_examples/services/export_csv/export_csv_invalid_fields_shared_examples.rb b/spec/support/shared_examples/services/export_csv/export_csv_invalid_fields_shared_examples.rb
new file mode 100644
index 00000000000..25899f93914
--- /dev/null
+++ b/spec/support/shared_examples/services/export_csv/export_csv_invalid_fields_shared_examples.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a service that returns invalid fields from selection' do
+ describe '#invalid_fields' do
+ it 'returns invalid fields from selection' do
+ fields = %w[title invalid_1 invalid_2]
+
+ service = described_class.new(WorkItem.all, project, fields)
+
+ expect(service.invalid_fields).to eq(%w[invalid_1 invalid_2])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb
index c38ca6a3bf0..4188c4aa56c 100644
--- a/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb
@@ -26,7 +26,9 @@ RSpec.shared_examples 'listing issuable discussions' do |user_role, internal_dis
discussions = next_page_discussions_service.execute
expect(discussions.count).to eq(2)
- expect(discussions.first.notes.map(&:note)).to match_array(["added #{label.to_reference} label"])
+ expect(discussions.first.notes.map(&:note)).to match_array(
+ ["added #{label.to_reference} #{label_2.to_reference} labels"]
+ )
expect(discussions.second.notes.map(&:note)).to match_array(["removed #{label.to_reference} label"])
end
end
@@ -93,7 +95,15 @@ def create_notes(issuable, note_body)
discussion_id: first_discussion.discussion_id, noteable: issuable,
project: issuable.project, note: "reply on #{note_body}")
- create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'add')
+ now = Time.current
+ create(
+ :resource_label_event,
+ user: current_user, "#{assoc_name}": issuable, label: label, action: 'add', created_at: now
+ )
+ create(
+ :resource_label_event,
+ user: current_user, "#{assoc_name}": issuable, label: label_2, action: 'add', created_at: now
+ )
create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'remove')
unless issuable.is_a?(Epic)
diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb
index 142d4ae8531..fe868d494d2 100644
--- a/spec/support/shared_examples/services/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_shared_examples.rb
@@ -11,7 +11,8 @@ end
RSpec.shared_examples 'updating a single task' do
def update_issuable(opts)
issuable = try(:issue) || try(:merge_request)
- described_class.new(project: project, current_user: user, params: opts).execute(issuable)
+ described_class.new(**described_class.constructor_container_arg(project), current_user: user, params: opts)
+ .execute(issuable)
end
before do
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index 4df12f7849b..bdb01b12607 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -12,27 +12,20 @@ RSpec.shared_examples 'misconfigured dashboard service response' do |status_code
end
RSpec.shared_examples 'valid dashboard service response for schema' do
- file_ref_resolver = proc do |uri|
- file = Rails.root.join(uri.path)
- raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json')
- raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist?
-
- Gitlab::Json.parse(File.read(file))
- end
-
it 'returns a json representation of the dashboard' do
result = service_call
expect(result.keys).to contain_exactly(:dashboard, :status)
expect(result[:status]).to eq(:success)
- validator = JSONSchemer.schema(dashboard_schema, ref_resolver: file_ref_resolver)
+ schema_path = Rails.root.join('spec/fixtures', dashboard_schema)
+ validator = JSONSchemer.schema(schema_path)
expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true
end
end
RSpec.shared_examples 'valid dashboard service response' do
- let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+ let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/dashboard.json' }
it_behaves_like 'valid dashboard service response for schema'
end
@@ -76,7 +69,7 @@ RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file
end
RSpec.shared_examples 'valid embedded dashboard service response' do
- let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
+ let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json' }
it_behaves_like 'valid dashboard service response for schema'
end
diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
index ea79dc674a1..a3042ac2e26 100644
--- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -4,6 +4,8 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
def check_release_files(expected_release_content)
distribution.reload
+ expect(expected_release_content).not_to include('MD5')
+
distribution.file.use_file do |file_path|
expect(File.read(file_path)).to eq(expected_release_content)
end
@@ -12,8 +14,10 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
expect(distribution.file_signature).to end_with("\n-----END PGP SIGNATURE-----\n")
distribution.signed_file.use_file do |file_path|
- expect(File.read(file_path)).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n#{expected_release_content}-----BEGIN PGP SIGNATURE-----\n")
- expect(File.read(file_path)).to end_with("\n-----END PGP SIGNATURE-----\n")
+ signed_file_content = File.read(file_path)
+ expect(signed_file_content).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash:")
+ expect(signed_file_content).to include("\n\n#{expected_release_content}-----BEGIN PGP SIGNATURE-----\n")
+ expect(signed_file_content).to end_with("\n-----END PGP SIGNATURE-----\n")
end
end
@@ -45,6 +49,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
expect(component_file.updated_at).to eq(release_date)
unless expected_content.nil?
+ expect(expected_content).not_to include('MD5')
component_file.file.use_file do |file_path|
expect(File.read(file_path)).to eq(expected_content)
end
@@ -73,9 +78,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
.and change { component_file1.reload.updated_at }.to(current_time.round)
package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a
- pool_prefix = 'pool/unstable'
+ pool_prefix = "pool/#{distribution.codename}"
pool_prefix += "/#{project.id}" if container_type == :group
- pool_prefix += "/p/#{package.name}/#{package.version}"
+ pool_prefix += "/#{package.name[0]}/#{package.name}/#{package.version}"
expected_main_amd64_content = <<~EOF
Package: libsample0
Source: #{package.name}
@@ -93,7 +98,6 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Priority: optional
Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
Size: 409600
- MD5sum: #{package_files[2].file_md5}
SHA256: #{package_files[2].file_sha256}
Package: sample-dev
@@ -113,7 +117,6 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Priority: optional
Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
Size: 409600
- MD5sum: #{package_files[3].file_md5}
SHA256: #{package_files[3].file_sha256}
EOF
@@ -122,7 +125,6 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
Priority: extra
Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
Size: 409600
- MD5sum: #{package_files[4].file_md5}
SHA256: #{package_files[4].file_sha256}
EOF
@@ -171,45 +173,26 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
check_component_file(current_time.round, 'contrib', :sources, nil, nil)
main_amd64_size = expected_main_amd64_content.length
- main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
contrib_all_size = component_file1.size
- contrib_all_md5sum = component_file1.file_md5
contrib_all_sha256 = component_file1.file_sha256
main_amd64_di_size = expected_main_amd64_di_content.length
- main_amd64_di_md5sum = Digest::MD5.hexdigest(expected_main_amd64_di_content)
main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content)
main_sources_size = expected_main_sources_content.length
- main_sources_md5sum = Digest::MD5.hexdigest(expected_main_sources_content)
main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content)
expected_release_content = <<~EOF
- Codename: unstable
+ Codename: #{distribution.codename}
Date: Sat, 25 Jan 2020 15:17:18 +0000
Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
Acquire-By-Hash: yes
Architectures: all amd64 arm64
Components: contrib main
- MD5Sum:
- #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Sources
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-all/Packages
- #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
- #{main_amd64_di_md5sum} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-arm64/Packages
- #{main_sources_md5sum} #{main_sources_size} main/source/Sources
SHA256:
- #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
+ #{contrib_all_sha256} #{contrib_all_size.to_s.rjust(8)} contrib/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages
@@ -218,12 +201,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages
- #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
- #{main_amd64_di_sha256} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages
+ #{main_amd64_sha256} #{main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages
+ #{main_amd64_di_sha256} #{main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages
- #{main_sources_sha256} #{main_sources_size} main/source/Sources
+ #{main_sources_sha256} #{main_sources_size.to_s.rjust(8)} main/source/Sources
EOF
+ expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite
check_release_files(expected_release_content)
end
@@ -247,13 +231,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do
.and not_change { distribution.component_files.reset.count }
expected_release_content = <<~EOF
- Codename: unstable
+ Codename: #{distribution.codename}
Date: Sat, 25 Jan 2020 15:17:18 +0000
Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
Acquire-By-Hash: yes
- MD5Sum:
SHA256:
EOF
+ expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite
check_release_files(expected_release_content)
end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index e0dd08ec50e..f63693dbf26 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'assigns build to package' do
context 'with build info' do
- let(:job) { create(:ci_build, user: user) }
+ let(:job) { create(:ci_build, user: user, project: project) }
let(:params) { super().merge(build: job) }
it 'assigns the pipeline to the package' do
diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
index a7e51408032..1383346644a 100644
--- a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'filters by paginated notes' do |event_type|
end
it 'only returns given notes' do
- paginated_notes = { event_type.to_s.pluralize => [double(id: event.id)] }
+ paginated_notes = { event_type.to_s.pluralize => [double(ids: [event.id])] }
notes = described_class.new(event.issue, user, paginated_notes: paginated_notes).execute
expect(notes.size).to eq(1)
diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
index 716be8c6210..209be09c807 100644
--- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
@@ -160,6 +160,21 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
end
end
end
+
+ context 'when the project is empty' do
+ let(:params) { nil }
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ it 'returns an error' do
+ expect { result }.to raise_error { |error|
+ expect(error).to be_a(Gitlab::Graphql::Errors::MutationError)
+ expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ 'href="http://localhost/help/user/project/repository/index.md' \
+ '#add-files-to-a-repository">add at least one file to the repository' \
+ '</a> before using Security features.')
+ }
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb
index 13a2aa9ddac..0f649173683 100644
--- a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb
+++ b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb
@@ -15,7 +15,8 @@ RSpec.shared_examples 'updating mentions' do |service_class|
def update_mentionable(opts)
perform_enqueued_jobs do
- service_class.new(project: project, current_user: user, params: opts).execute(mentionable)
+ service_class.new(**service_class.constructor_container_arg(project),
+ current_user: user, params: opts).execute(mentionable)
end
mentionable.reload
diff --git a/spec/support/shared_examples/views/themed_layout_examples.rb b/spec/support/shared_examples/views/themed_layout_examples.rb
index b6c53dce4cb..599fd141dd7 100644
--- a/spec/support/shared_examples/views/themed_layout_examples.rb
+++ b/spec/support/shared_examples/views/themed_layout_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples "a layout which reflects the application theme setting", :themed_layout do
+RSpec.shared_examples "a layout which reflects the application theme setting" do
context 'as a themed layout' do
let(:default_theme_class) { ::Gitlab::Themes.default.css_class }
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 b1011037584..1703d400aea 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -15,6 +15,22 @@ RSpec.shared_examples 'work item base types importer' do
expect(WorkItems::Type.all).to all(be_valid)
end
+ it 'creates all default widget definitions' do
+ WorkItems::WidgetDefinition.delete_all
+ widget_mapping = ::Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter::WIDGETS_FOR_TYPE
+
+ expect { subject }.to change { WorkItems::WidgetDefinition.count }.from(0).to(widget_mapping.values.flatten.count)
+
+ created_widgets = WorkItems::WidgetDefinition.global.map do |widget|
+ { name: widget.work_item_type.name, type: widget.widget_type }
+ end
+ expected_widgets = widget_mapping.flat_map do |type_sym, widget_types|
+ widget_types.map { |type| { name: ::WorkItems::Type::TYPE_NAMES[type_sym], type: type.to_s } }
+ end
+
+ expect(created_widgets).to match_array(expected_widgets)
+ end
+
it 'upserts base work item types if they already exist' do
first_type = WorkItems::Type.first
original_name = first_type.name
@@ -29,8 +45,34 @@ RSpec.shared_examples 'work item base types importer' do
)
end
- it 'executes a single INSERT query' do
- expect { subject }.to make_queries_matching(/INSERT/, 1)
+ it 'upserts default widget definitions if they already exist and type changes' do
+ widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+ widget.update!(widget_type: :weight)
+
+ expect do
+ subject
+ widget.reload
+ end.to not_change(WorkItems::WidgetDefinition, :count).and(
+ change { widget.widget_type }.from('weight').to('labels')
+ )
+ end
+
+ it 'does not change default widget definitions if they already exist with changed disabled status' do
+ widget = WorkItems::WidgetDefinition.global.find_by_widget_type(:labels)
+
+ widget.update!(disabled: true)
+
+ expect do
+ subject
+ widget.reload
+ end.to not_change(WorkItems::WidgetDefinition, :count).and(
+ not_change { widget.disabled }
+ )
+ end
+
+ it 'executes single INSERT query per types and widget definitions' do
+ expect { subject }.to make_queries_matching(/INSERT/, 2)
end
context 'when some base types exist' do
@@ -39,10 +81,22 @@ RSpec.shared_examples 'work item base types importer' do
end
it 'inserts all types and does nothing if some already existed' do
- expect { subject }.to make_queries_matching(/INSERT/, 1).and(
+ expect { subject }.to make_queries_matching(/INSERT/, 2).and(
change { WorkItems::Type.count }.by(1)
)
expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count)
end
end
+
+ context 'when some widget definitions exist' do
+ before do
+ WorkItems::WidgetDefinition.limit(1).delete_all
+ end
+
+ it 'inserts all widget definitions and does nothing if some already existed' do
+ expect { subject }.to make_queries_matching(/INSERT/, 2).and(
+ change { WorkItems::WidgetDefinition.count }.by(1)
+ )
+ end
+ end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
index ae29b76ee87..e224b71da91 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
@@ -101,7 +101,7 @@ RSpec.shared_examples 'batched background migrations execution worker' do
context 'when the provided database is sharing config' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'does nothing' do
diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
index ba1bdfa7aa8..abb00efdee8 100644
--- a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
+++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
@@ -147,5 +147,19 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
subject.perform(resource.id, 'prune', lease_key, lease_uuid)
end
end
+
+ context 'eager' do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ specify do
+ expect_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
+ expect(instance).to receive(:optimize_repository).with(eager: true).and_call_original
+ end
+
+ subject.perform(resource.id, 'eager', lease_key, lease_uuid)
+ end
+ end
end
end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index b9bd3f82f65..171c7ace2d2 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -9,12 +9,26 @@ def webmock_allowed_hosts
hosts << URI.parse(ENV['ELASTIC_URL']).host
end
+ if ENV.key?('ZOEKT_INDEX_BASE_URL')
+ hosts.concat(allowed_host_and_ip(ENV['ZOEKT_INDEX_BASE_URL']))
+ end
+
+ if ENV.key?('ZOEKT_SEARCH_BASE_URL')
+ hosts.concat(allowed_host_and_ip(ENV['ZOEKT_SEARCH_BASE_URL']))
+ end
+
if Gitlab.config.webpack&.dev_server&.enabled
hosts << Gitlab.config.webpack.dev_server.host
end
end.compact.uniq
end
+def allowed_host_and_ip(url)
+ host = URI.parse(url).host
+ ip_address = Addrinfo.ip(host).ip_address
+ [host, ip_address]
+end
+
def with_net_connect_allowed
WebMock.allow_net_connect!
yield
diff --git a/spec/support_specs/database/multiple_databases_helpers_spec.rb b/spec/support_specs/database/multiple_databases_helpers_spec.rb
index eb0e980d376..2577b64f214 100644
--- a/spec/support_specs/database/multiple_databases_helpers_spec.rb
+++ b/spec/support_specs/database/multiple_databases_helpers_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Database::MultipleDatabasesHelpers' do
context 'on Ci::ApplicationRecord' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'raises exception' do
@@ -83,7 +83,7 @@ RSpec.describe 'Database::MultipleDatabasesHelpers' do
describe '.with_added_ci_connection' do
context 'when only a single database is setup' do
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'connects Ci::ApplicationRecord to the main database for the duration of the block', :aggregate_failures do
@@ -100,7 +100,7 @@ RSpec.describe 'Database::MultipleDatabasesHelpers' do
context 'when multiple databases are setup' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'does not mock the original Ci::ApplicationRecord connection', :aggregate_failures do
diff --git a/spec/support_specs/helpers/migrations_helpers_spec.rb b/spec/support_specs/helpers/migrations_helpers_spec.rb
index b82eddad9bc..5d44dac8eb7 100644
--- a/spec/support_specs/helpers/migrations_helpers_spec.rb
+++ b/spec/support_specs/helpers/migrations_helpers_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe MigrationsHelpers do
context 'ci database configured' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'returns the CI base model' do
@@ -30,7 +30,7 @@ RSpec.describe MigrationsHelpers do
context 'ci database not configured' do
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'returns the main base model' do
@@ -51,7 +51,7 @@ RSpec.describe MigrationsHelpers do
context 'ci database configured' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'create a class based on the CI base model' do
@@ -62,7 +62,7 @@ RSpec.describe MigrationsHelpers do
context 'ci database not configured' do
before do
- skip_if_multiple_databases_are_setup
+ skip_if_multiple_databases_are_setup(:ci)
end
it 'creates a class based on main base model' do
diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb
index 9b6ea3891d9..375d01bf2ba 100644
--- a/spec/tasks/cache/clear/redis_spec.rb
+++ b/spec/tasks/cache/clear/redis_spec.rb
@@ -3,7 +3,7 @@
require 'rake_helper'
RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :clean_gitlab_redis_cache,
- :silence_stdout, feature_category: :redis do
+ :silence_stdout, :use_null_store_as_repository_cache, feature_category: :redis do
before do
Rake.application.rake_require 'tasks/cache'
end
@@ -20,37 +20,11 @@ RSpec.describe 'clearing redis cache', :clean_gitlab_redis_repository_cache, :cl
create(:ci_pipeline, project: project).project.pipeline_status
end
- context 'when use_primary_and_secondary_stores_for_repository_cache MultiStore FF is enabled' do
- # Initially, project:{id}:pipeline_status is explicitly cached in Gitlab::Redis::Cache, whereas repository is
- # cached in Rails.cache (which is a NullStore).
- # With the MultiStore feature flag enabled, we use Gitlab::Redis::RepositoryCache instance as primary store and
- # Gitlab::Redis::Cache as secondary store.
- # This ends up storing 2 extra keys (exists? and root_ref) in both Gitlab::Redis::RepositoryCache and
- # Gitlab::Redis::Cache instances when loading project.pipeline_status
- let(:keys_size_changed) { -3 }
-
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: true)
- allow(pipeline_status).to receive(:loaded).and_return(nil)
- end
-
- it 'clears pipeline status cache' do
- expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? }
- end
-
- it_behaves_like 'clears the cache'
+ before do
+ allow(pipeline_status).to receive(:loaded).and_return(nil)
end
- context 'when use_primary_and_secondary_stores_for_repository_cache and
- use_primary_store_as_default_for_repository_cache feature flags are disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_repository_cache: false)
- stub_feature_flags(use_primary_store_as_default_for_repository_cache: false)
- allow(pipeline_status).to receive(:loaded).and_return(nil)
- end
-
- it_behaves_like 'clears the cache'
- end
+ it_behaves_like 'clears the cache'
end
describe 'clearing set caches' do
diff --git a/spec/tasks/dev_rake_spec.rb b/spec/tasks/dev_rake_spec.rb
index a09756b862e..ef047b383a6 100644
--- a/spec/tasks/dev_rake_spec.rb
+++ b/spec/tasks/dev_rake_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe 'dev rake tasks' do
context 'multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
context 'with a valid database' do
diff --git a/spec/tasks/gitlab/background_migrations_rake_spec.rb b/spec/tasks/gitlab/background_migrations_rake_spec.rb
index d8ce00a65e6..876b56d1208 100644
--- a/spec/tasks/gitlab/background_migrations_rake_spec.rb
+++ b/spec/tasks/gitlab/background_migrations_rake_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe 'gitlab:background_migrations namespace rake tasks', :suppress_gi
let(:databases) { [Gitlab::Database::MAIN_DATABASE_NAME, ci_database_name] }
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 972851cba8c..c0196c09e3c 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -2,14 +2,12 @@
require 'rake_helper'
-RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :backup_restore do
+RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category: :backup_restore do
let(:enable_registry) { true }
let(:backup_restore_pid_path) { "#{Rails.application.root}/tmp/backup_restore.pid" }
let(:backup_tasks) { %w[db repo uploads builds artifacts pages lfs terraform_state registry packages] }
let(:backup_types) do
- %w[main_db repositories uploads builds artifacts pages lfs terraform_state registry packages].tap do |array|
- array.insert(1, 'ci_db') if Gitlab::Database.has_config?(:ci)
- end
+ %w[db repositories uploads builds artifacts pages lfs terraform_state registry packages]
end
def tars_glob
@@ -94,7 +92,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :bac
let(:pid_file) { instance_double(File, write: 12345) }
where(:tasks_name, :rake_task) do
- %w[main_db ci_db] | 'gitlab:backup:db:restore'
+ 'db' | 'gitlab:backup:db:restore'
'repositories' | 'gitlab:backup:repo:restore'
'builds' | 'gitlab:backup:builds:restore'
'uploads' | 'gitlab:backup:uploads:restore'
@@ -260,9 +258,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete, feature_category: :bac
end
it 'logs the progress to log file' do
- ci_database_status = Gitlab::Database.has_config?(:ci) ? "[SKIPPED]" : "[DISABLED]"
- expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping main_database ... [SKIPPED]")
- expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping ci_database ... #{ci_database_status}")
+ expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping database ... [SKIPPED]")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ... ")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping repositories ... done")
expect(Gitlab::BackupLogger).to receive(:info).with(message: "Dumping uploads ... ")
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index aee03059120..74cc5dd6d7c 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'check.rake', :silence_stdout do
+RSpec.describe 'check.rake', :silence_stdout, feature_category: :gitaly do
before do
Rake.application.rake_require 'tasks/gitlab/check'
diff --git a/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb b/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb
index 9cdbf8539c6..0682a4b39cf 100644
--- a/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb
+++ b/spec/tasks/gitlab/db/decomposition/rollback/bump_ci_sequences_rake_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'gitlab:db:decomposition:rollback:bump_ci_sequences', :silence_st
context 'when multiple databases' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
end
it 'does not change ci sequences on the ci database' do
diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
index a0a99b65767..9d54241aa7f 100644
--- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
+++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
@@ -2,8 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_record_base, :delete,
- :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
+RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, feature_category: :pods do
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
@@ -14,193 +13,126 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
Rake::Task.define_task :environment
end
- let(:main_connection) { ApplicationRecord.connection }
- let(:ci_connection) { Ci::ApplicationRecord.connection }
- let!(:user) { create(:user) }
- let!(:ci_build) { create(:ci_build) }
-
- let(:detached_partition_table) { '_test_gitlab_main_part_20220101' }
+ let(:table_locker) { instance_double(Gitlab::Database::TablesLocker) }
+ let(:logger) { instance_double(Logger, level: nil) }
+ let(:dry_run) { false }
+ let(:verbose) { false }
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(Logger).to receive(:new).with($stdout).and_return(logger)
+ allow(Gitlab::Database::TablesLocker).to receive(:new).with(
+ logger: logger, dry_run: dry_run
+ ).and_return(table_locker)
end
- after do
- run_rake_task('gitlab:db:unlock_writes')
- end
+ shared_examples "call table locker" do |method|
+ let(:log_level) { verbose ? Logger::INFO : Logger::WARN }
- after(:all) do
- drop_detached_partition_sql = <<~SQL
- DROP TABLE IF EXISTS gitlab_partitions_dynamic._test_gitlab_main_part_20220101
- SQL
+ it "creates TablesLocker with dry run set and calls #{method}" do
+ expect(logger).to receive(:level=).with(log_level)
+ expect(table_locker).to receive(method)
- ApplicationRecord.connection.execute(drop_detached_partition_sql)
- Ci::ApplicationRecord.connection.execute(drop_detached_partition_sql)
-
- Gitlab::Database::SharedModel.using_connection(ApplicationRecord.connection) do
- Postgresql::DetachedPartition.delete_all
+ run_rake_task("gitlab:db:#{method}")
end
end
- context 'single database' do
- before do
- skip_if_multiple_databases_are_setup
- end
+ describe 'lock_writes' do
+ context 'when environment sets DRY_RUN to true' do
+ let(:dry_run) { true }
- context 'when locking writes' do
- it 'does not add any triggers to the main schema tables' do
- expect do
- run_rake_task('gitlab:db:lock_writes')
- end.to change {
- number_of_triggers(main_connection)
- }.by(0)
+ before do
+ stub_env('DRY_RUN', 'true')
end
- it 'will be still able to modify tables that belong to the main two schemas' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- User.last.touch
- Ci::Build.last.touch
- end.not_to raise_error
- end
+ include_examples "call table locker", :lock_writes
end
- end
- context 'multiple databases' do
- before do
- skip_if_multiple_databases_not_setup
+ context 'when environment sets DRY_RUN to false' do
+ let(:dry_run) { false }
- Gitlab::Database::SharedModel.using_connection(ci_connection) do
- Postgresql::DetachedPartition.create!(
- table_name: detached_partition_table,
- drop_after: Time.current
- )
+ before do
+ stub_env('DRY_RUN', 'false')
end
+
+ include_examples "call table locker", :lock_writes
end
- context 'when locking writes' do
- it 'still allows writes on the tables with the correct connections' do
- User.update_all(updated_at: Time.now)
- Ci::Build.update_all(updated_at: Time.now)
- end
+ context 'when environment does not define DRY_RUN' do
+ let(:dry_run) { false }
- it 'still allows writing to gitlab_shared schema on any connection' do
- connections = [main_connection, ci_connection]
- connections.each do |connection|
- Gitlab::Database::SharedModel.using_connection(connection) do
- LooseForeignKeys::DeletedRecord.create!(
- fully_qualified_table_name: "public.users",
- primary_key_value: 1,
- cleanup_attempts: 0
- )
- end
- end
- end
+ include_examples "call table locker", :lock_writes
+ end
- it 'prevents writes on the main tables on the ci database' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- ci_connection.execute("delete from users")
- end.to raise_error(ActiveRecord::StatementInvalid, /Table: "users" is write protected/)
- end
+ context 'when environment sets VERBOSE to true' do
+ let(:verbose) { true }
- it 'prevents writes on the ci tables on the main database' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- main_connection.execute("delete from ci_builds")
- end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_builds" is write protected/)
+ before do
+ stub_env('VERBOSE', 'true')
end
- it 'prevents truncating a ci table on the main database' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- main_connection.execute("truncate ci_build_needs")
- end.to raise_error(ActiveRecord::StatementInvalid, /Table: "ci_build_needs" is write protected/)
- end
+ include_examples "call table locker", :lock_writes
+ end
+
+ context 'when environment sets VERBOSE to false' do
+ let(:verbose) { false }
- 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/)
+ before do
+ stub_env('VERBOSE', 'false')
end
+
+ include_examples "call table locker", :lock_writes
end
- context 'when running in dry_run mode' do
+ context 'when environment does not define VERBOSE' do
+ include_examples "call table locker", :lock_writes
+ end
+ end
+
+ describe 'unlock_writes' do
+ context 'when environment sets DRY_RUN to true' do
+ let(:dry_run) { true }
+
before do
stub_env('DRY_RUN', 'true')
end
- it 'allows writes on the main tables on the ci database' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- ci_connection.execute("delete from users")
- end.not_to raise_error
- end
-
- it 'allows writes on the ci tables on the main database' do
- run_rake_task('gitlab:db:lock_writes')
- expect do
- main_connection.execute("delete from ci_builds")
- end.not_to raise_error
- end
+ include_examples "call table locker", :unlock_writes
end
- context 'multiple shared databases' do
+ context 'when environment sets DRY_RUN to false' do
before do
- allow(::Gitlab::Database).to receive(:db_config_share_with).and_return(nil)
- ci_db_config = Ci::ApplicationRecord.connection_db_config
- allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
+ stub_env('DRY_RUN', 'false')
end
- it 'does not lock any tables if the ci database is shared with main database' do
- run_rake_task('gitlab:db:lock_writes')
+ include_examples "call table locker", :unlock_writes
+ end
- expect do
- ApplicationRecord.connection.execute("delete from ci_builds")
- Ci::ApplicationRecord.connection.execute("delete from users")
- end.not_to raise_error
- end
+ context 'when environment does not define DRY_RUN' do
+ include_examples "call table locker", :unlock_writes
end
- context 'when unlocking writes' do
+ context 'when environment sets VERBOSE to true' do
+ let(:verbose) { true }
+
before do
- run_rake_task('gitlab:db:lock_writes')
+ stub_env('VERBOSE', 'true')
end
- it 'allows writes again on the gitlab_ci tables on the main database' do
- run_rake_task('gitlab:db:unlock_writes')
-
- expect do
- main_connection.execute("delete from ci_builds")
- end.not_to raise_error
- end
+ include_examples "call table locker", :lock_writes
+ end
- it 'allows writes again to detached partitions' do
- run_rake_task('gitlab:db:unlock_writes')
+ context 'when environment sets VERBOSE to false' do
+ let(:verbose) { false }
- expect do
- ci_connection.execute("INSERT INTO gitlab_partitions_dynamic._test_gitlab_main_part_20220101 DEFAULT VALUES")
- end.not_to raise_error
+ before do
+ stub_env('VERBOSE', 'false')
end
+
+ include_examples "call table locker", :lock_writes
end
- end
- def number_of_triggers(connection)
- connection.select_value("SELECT count(*) FROM information_schema.triggers")
+ context 'when environment does not define VERBOSE' do
+ include_examples "call table locker", :lock_writes
+ end
end
end
diff --git a/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb b/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb
index a7ced4a69f3..6e245b6f227 100644
--- a/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb
+++ b/spec/tasks/gitlab/db/truncate_legacy_tables_rake_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'gitlab:db:truncate_legacy_tables', :silence_stdout, :reestablish
end
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
# Filling the table on both databases main and ci
Gitlab::Database.database_base_models.each_value do |base_model|
diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
index 1d47c94aa77..cc90345c7e0 100644
--- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb
+++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection do
+RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
# We don't need to delete this data since it only modifies `ar_internal_metadata`
# which would not be cleaned either by `DbCleaner`
self.use_transactional_tests = false
@@ -235,12 +235,4 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_sc
end
end
end
-
- def with_db_configs(test: test_config)
- current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases
- ActiveRecord::Base.configurations = { test: test_config }
- yield
- ensure
- ActiveRecord::Base.configurations = current_configurations
- end
end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 7671c65d22c..933eba40719 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'rake'
-RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
+RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_category: :database do
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
@@ -352,6 +352,101 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
+ describe 'dictionary generate' do
+ let(:db_config) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, name: 'fake_db') }
+
+ let(:model) { ActiveRecord::Base }
+ let(:connection) { model.connection }
+
+ let(:base_models) { { 'fake_db' => model }.with_indifferent_access }
+
+ let(:tables) { %w[table1 _test_dictionary_table_name] }
+ let(:views) { %w[view1] }
+
+ let(:table_file_path) { 'db/docs/table1.yml' }
+ let(:view_file_path) { 'db/docs/views/view1.yml' }
+ let(:test_table_path) { 'db/docs/_test_dictionary_table_name.yml' }
+
+ before do
+ allow(Gitlab::Database).to receive(:db_config_for_connection).and_return(db_config)
+ allow(Gitlab::Database).to receive(:database_base_models).and_return(base_models)
+
+ allow(connection).to receive(:tables).and_return(tables)
+ allow(connection).to receive(:views).and_return(views)
+ end
+
+ after do
+ File.delete(table_file_path)
+ File.delete(view_file_path)
+ end
+
+ context 'when the dictionary files do not exist' do
+ it 'generate the dictionary files' do
+ run_rake_task('gitlab:db:dictionary:generate')
+
+ expect(File).to exist(File.join(table_file_path))
+ expect(File).to exist(File.join(view_file_path))
+ end
+
+ it 'do not generate the dictionary files for test tables' do
+ run_rake_task('gitlab:db:dictionary:generate')
+
+ expect(File).not_to exist(File.join(test_table_path))
+ end
+ end
+
+ context 'when the dictionary files already exist' do
+ let(:table_class) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'table1'
+ end
+ end
+
+ let(:view_class) do
+ Class.new(ApplicationRecord) do
+ self.table_name = 'view1'
+ end
+ end
+
+ table_metadata = {
+ 'table_name' => 'table1',
+ 'classes' => [],
+ 'feature_categories' => [],
+ 'description' => nil,
+ 'introduced_by_url' => nil,
+ 'milestone' => 14.3
+ }
+ view_metadata = {
+ 'view_name' => 'view1',
+ 'classes' => [],
+ 'feature_categories' => [],
+ 'description' => nil,
+ 'introduced_by_url' => nil,
+ 'milestone' => 14.3
+ }
+
+ before do
+ stub_const('TableClass', table_class)
+ stub_const('ViewClass', view_class)
+
+ File.write(table_file_path, table_metadata.to_yaml)
+ File.write(view_file_path, view_metadata.to_yaml)
+
+ allow(model).to receive(:descendants).and_return([table_class, view_class])
+ end
+
+ it 'update the dictionary content' do
+ run_rake_task('gitlab:db:dictionary:generate')
+
+ table_metadata = YAML.safe_load(File.read(table_file_path))
+ expect(table_metadata['classes']).to match_array(['TableClass'])
+
+ view_metadata = YAML.safe_load(File.read(view_file_path))
+ expect(view_metadata['classes']).to match_array(['ViewClass'])
+ end
+ end
+ end
+
describe 'unattended' do
using RSpec::Parameterized::TableSyntax
@@ -573,7 +668,7 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
let(:base_models) { { 'main' => double(:model), 'ci' => double(:model) } }
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared).and_return(base_models)
end
@@ -636,6 +731,80 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
+ describe 'execute_async_index_operations' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'delegates ci task to Gitlab::Database::AsyncIndexes' do
+ expect(Gitlab::Database::AsyncIndexes).to receive(:execute_pending_actions!).with(how_many: 2)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:ci')
+ end
+
+ it 'delegates ci task to Gitlab::Database::AsyncIndexes with specified argument' do
+ expect(Gitlab::Database::AsyncIndexes).to receive(:execute_pending_actions!).with(how_many: 5)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:ci', '[5]')
+ end
+
+ it 'delegates main task to Gitlab::Database::AsyncIndexes' do
+ expect(Gitlab::Database::AsyncIndexes).to receive(:execute_pending_actions!).with(how_many: 2)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:main')
+ end
+
+ it 'delegates main task to Gitlab::Database::AsyncIndexes with specified argument' do
+ expect(Gitlab::Database::AsyncIndexes).to receive(:execute_pending_actions!).with(how_many: 7)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:main', '[7]')
+ end
+
+ it 'delegates all task to every database with higher default for dev' do
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:ci']).to receive(:invoke).with(1000)
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:main']).to receive(:invoke).with(1000)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:all')
+ end
+
+ it 'delegates all task to every database with lower default for prod' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:ci']).to receive(:invoke).with(2)
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:main']).to receive(:invoke).with(2)
+
+ run_rake_task('gitlab:db:execute_async_index_operations:all')
+ end
+
+ it 'delegates all task to every database with specified argument' do
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:ci']).to receive(:invoke).with('50')
+ expect(Rake::Task['gitlab:db:execute_async_index_operations:main']).to receive(:invoke).with('50')
+
+ run_rake_task('gitlab:db:execute_async_index_operations:all', '[50]')
+ end
+
+ context 'when feature is not enabled' do
+ it 'is a no-op' do
+ stub_feature_flags(database_async_index_operations: false)
+
+ expect(Gitlab::Database::AsyncIndexes).not_to receive(:execute_pending_actions!)
+
+ expect { run_rake_task('gitlab:db:execute_async_index_operations:main') }.to raise_error(SystemExit)
+ end
+ end
+
+ context 'with geo configured' do
+ before do
+ skip_unless_geo_configured
+ end
+
+ it 'does not create a task for the geo database' do
+ expect { run_rake_task('gitlab:db:execute_async_index_operations:geo') }
+ .to raise_error(/Don't know how to build task 'gitlab:db:execute_async_index_operations:geo'/)
+ end
+ end
+ end
+
describe 'active' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/tasks/gitlab/incoming_email_rake_spec.rb b/spec/tasks/gitlab/incoming_email_rake_spec.rb
new file mode 100644
index 00000000000..3e1cc663ddb
--- /dev/null
+++ b/spec/tasks/gitlab/incoming_email_rake_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:incoming_email:secret rake tasks', :silence_stdout, feature_category: :build do
+ let(:encrypted_secret_file_dir) { Pathname.new(Dir.mktmpdir) }
+ let(:encrypted_secret_file) { encrypted_secret_file_dir.join('incoming_email.yaml.enc') }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/incoming_email'
+ stub_env('EDITOR', 'cat')
+ stub_warn_user_is_not_gitlab
+ allow(Gitlab.config.incoming_email).to receive(:encrypted_secret_file).and_return(encrypted_secret_file)
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ end
+
+ after do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/incoming_email_enc'))
+ end
+
+ describe ':show' do
+ it 'displays error when file does not exist' do
+ expect { run_rake_task('gitlab:incoming_email:secret:show') }.to \
+ output(/File .* does not exist. Use `gitlab-rake gitlab:incoming_email:secret:edit` to change that./).to_stdout
+ end
+
+ it 'displays error when key does not exist' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:incoming_email:secret:show') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect { run_rake_task('gitlab:incoming_email:secret:show') }.to \
+ output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'outputs the unencrypted content when present' do
+ encrypted = Settings.encrypted(encrypted_secret_file)
+ encrypted.write('somevalue')
+ expect { run_rake_task('gitlab:incoming_email:secret:show') }.to output(/somevalue/).to_stdout
+ end
+ end
+
+ describe 'edit' do
+ it 'creates encrypted file' do
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(encrypted_secret_file)).to be true
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/password: '123'/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(encrypted_secret_file_dir)
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/WARNING: Content was not a valid INCOMING_EMAIL secret yml file/).to_stdout
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+
+ it 'displays error when $EDITOR is not set' do
+ stub_env('EDITOR', nil)
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/No \$EDITOR specified to open file. Please provide one when running the command/).to_stderr
+ end
+ end
+
+ describe 'write' do
+ before do
+ allow($stdin).to receive(:tty?).and_return(false)
+ allow($stdin).to receive(:read).and_return('username: foo')
+ end
+
+ it 'creates encrypted file from stdin' do
+ expect { run_rake_task('gitlab:incoming_email:secret:write') }.to output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(encrypted_secret_file)).to be true
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/username: foo/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:incoming_email:secret:write') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(encrypted_secret_file_dir)
+ expect { run_rake_task('gitlab:incoming_email:secret:write') }.to output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ expect { run_rake_task('gitlab:incoming_email:secret:edit') }.to \
+ output(/WARNING: Content was not a valid INCOMING_EMAIL secret yml file/).to_stdout
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+ end
+end
diff --git a/spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb b/spec/tasks/gitlab/metrics_exporter_task_spec.rb
index 4e17e91f019..ca37fc1b5d7 100644
--- a/spec/lib/tasks/gitlab/metrics_exporter_task_spec.rb
+++ b/spec/tasks/gitlab/metrics_exporter_task_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'rake_helper'
-require_relative '../../../support/helpers/next_instance_of'
+require_relative '../../support/helpers/next_instance_of'
-RSpec.describe 'gitlab:metrics_exporter:install' do
+RSpec.describe 'gitlab:metrics_exporter:install', feature_category: :metrics do
before do
Rake.application.rake_require 'tasks/gitlab/metrics_exporter'
end
@@ -21,7 +21,7 @@ RSpec.describe 'gitlab:metrics_exporter:install' do
end
context 'when target directory is specified' do
- let(:args) { Rake::TaskArguments.new(%w(dir), %w(path/to/exporter)) }
+ let(:args) { Rake::TaskArguments.new(%w[dir], %w[path/to/exporter]) }
let(:context) { TOPLEVEL_BINDING.eval('self') }
let(:expected_clone_params) do
{
diff --git a/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb b/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
index 85f71da8c97..25ea5d75a56 100644
--- a/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
+++ b/spec/tasks/gitlab/security/update_banned_ssh_keys_rake_spec.rb
@@ -7,7 +7,7 @@ require 'rake_helper'
# is hit in the rake task.
require 'git'
-RSpec.describe 'gitlab:security namespace rake tasks', :silence_stdout, feature_category: :security do
+RSpec.describe 'gitlab:security namespace rake tasks', :silence_stdout, feature_category: :credential_management do
let(:fixture_path) { Rails.root.join('spec/fixtures/tasks/gitlab/security') }
let(:output_file) { File.join(__dir__, 'tmp/banned_keys_test.yml') }
let(:git_url) { 'https://github.com/rapid7/ssh-badkeys.git' }
diff --git a/spec/tasks/gitlab/seed/group_seed_rake_spec.rb b/spec/tasks/gitlab/seed/group_seed_rake_spec.rb
index 2f57e875f5f..43351031414 100644
--- a/spec/tasks/gitlab/seed/group_seed_rake_spec.rb
+++ b/spec/tasks/gitlab/seed/group_seed_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:seed:group_seed rake task', :silence_stdout do
+RSpec.describe 'gitlab:seed:group_seed rake task', :silence_stdout, feature_category: :subgroups do
let(:username) { 'group_seed' }
let!(:user) { create(:user, username: username) }
let(:task_params) { [2, username] }
diff --git a/spec/tasks/gitlab/service_desk_email_rake_spec.rb b/spec/tasks/gitlab/service_desk_email_rake_spec.rb
new file mode 100644
index 00000000000..6a1a7473f4a
--- /dev/null
+++ b/spec/tasks/gitlab/service_desk_email_rake_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:service_desk_email:secret rake tasks', :silence_stdout, feature_category: :build do
+ let(:encrypted_secret_file_dir) { Pathname.new(Dir.mktmpdir) }
+ let(:encrypted_secret_file) { encrypted_secret_file_dir.join('service_desk_email.yaml.enc') }
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/service_desk_email'
+ stub_env('EDITOR', 'cat')
+ stub_warn_user_is_not_gitlab
+ FileUtils.mkdir_p('tmp/tests/service_desk_email_enc/')
+ allow(Gitlab.config.service_desk_email).to receive(:encrypted_secret_file).and_return(encrypted_secret_file)
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ end
+
+ after do
+ FileUtils.rm_rf(Rails.root.join('tmp/tests/service_desk_email_enc'))
+ end
+
+ describe ':show' do
+ it 'displays error when file does not exist' do
+ expect { run_rake_task('gitlab:service_desk_email:secret:show') }.to \
+ output(/File .* does not exist. Use `gitlab-rake gitlab:service_desk_email:secret:edit` to change that./) \
+ .to_stdout
+ end
+
+ it 'displays error when key does not exist' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:service_desk_email:secret:show') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect { run_rake_task('gitlab:service_desk_email:secret:show') }.to \
+ output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'outputs the unencrypted content when present' do
+ encrypted = Settings.encrypted(encrypted_secret_file)
+ encrypted.write('somevalue')
+ expect { run_rake_task('gitlab:service_desk_email:secret:show') }.to output(/somevalue/).to_stdout
+ end
+ end
+
+ describe 'edit' do
+ it 'creates encrypted file' do
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(encrypted_secret_file)).to be true
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/password: '123'/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when key is changed' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(SecureRandom.hex(64))
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/Couldn't decrypt .* Perhaps you passed the wrong key?/).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(encrypted_secret_file_dir)
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/WARNING: Content was not a valid SERVICE_DESK_EMAIL secret yml file/).to_stdout
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+
+ it 'displays error when $EDITOR is not set' do
+ stub_env('EDITOR', nil)
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/No \$EDITOR specified to open file. Please provide one when running the command/).to_stderr
+ end
+ end
+
+ describe 'write' do
+ before do
+ allow($stdin).to receive(:tty?).and_return(false)
+ allow($stdin).to receive(:read).and_return('username: foo')
+ end
+
+ it 'creates encrypted file from stdin' do
+ expect { run_rake_task('gitlab:service_desk_email:secret:write') }.to \
+ output(/File encrypted and saved./).to_stdout
+ expect(File.exist?(encrypted_secret_file)).to be true
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/username: foo/)
+ end
+
+ it 'displays error when key does not exist' do
+ allow(Gitlab::Application.secrets).to receive(:encrypted_settings_key_base).and_return(nil)
+ expect { run_rake_task('gitlab:service_desk_email:secret:write') }.to \
+ output(/Missing encryption key encrypted_settings_key_base./).to_stderr
+ end
+
+ it 'displays error when write directory does not exist' do
+ FileUtils.rm_rf(encrypted_secret_file_dir)
+ expect { run_rake_task('gitlab:service_desk_email:secret:write') }.to \
+ output(/Directory .* does not exist./).to_stderr
+ end
+
+ it 'shows a warning when content is invalid' do
+ Settings.encrypted(encrypted_secret_file).write('somevalue')
+ expect { run_rake_task('gitlab:service_desk_email:secret:edit') }.to \
+ output(/WARNING: Content was not a valid SERVICE_DESK_EMAIL secret yml file/).to_stdout
+ value = Settings.encrypted(encrypted_secret_file)
+ expect(value.read).to match(/somevalue/)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 38a031178ae..a2546b8d033 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'rake gitlab:storage:*', :silence_stdout do
+RSpec.describe 'rake gitlab:storage:*', :silence_stdout, feature_category: :pods do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index 95ebaf6ea24..72f284b0b7f 100644
--- a/spec/tasks/gitlab/usage_data_rake_spec.rb
+++ b/spec/tasks/gitlab/usage_data_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
+RSpec.describe 'gitlab:usage data take tasks', :silence_stdout, feature_category: :service_ping do
include StubRequests
include UsageDataHelpers
@@ -78,7 +78,8 @@ RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
`git checkout -- #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}`
end
- it "generates #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}" do
+ it "generates #{Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH}",
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/386191' do
run_rake_task('gitlab:usage_data:generate_ci_template_events')
expect(File.exist?(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH)).to be true
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 6b5985a2a8a..4255e16b0e4 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:workhorse namespace rake task', :silence_stdout do
+RSpec.describe 'gitlab:workhorse namespace rake task', :silence_stdout, feature_category: :source_code_management do
before :all do
Rake.application.rake_require 'tasks/gitlab/workhorse'
end
diff --git a/spec/tasks/import_rake_spec.rb b/spec/tasks/import_rake_spec.rb
new file mode 100644
index 00000000000..31ce9e124c8
--- /dev/null
+++ b/spec/tasks/import_rake_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'import:github rake tasks', feature_category: :importers do
+ before do
+ Rake.application.rake_require 'tasks/import'
+ end
+
+ describe ':import' do
+ let(:user) { create(:user) }
+ let(:user_name) { user.username }
+ let(:github_repo) { 'github_user/repo' }
+ let(:target_namespace) { user.namespace_path }
+ let(:project_path) { "#{target_namespace}/project_name" }
+
+ before do
+ allow($stdin).to receive(:getch)
+
+ stub_request(:get, 'https://api.github.com/user/repos?per_page=100')
+ .to_return(
+ status: 200,
+ body: [{ id: 1, full_name: 'github_user/repo', clone_url: 'https://github.com/user/repo.git' }].to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ context 'when importing a single project' do
+ subject(:import_task) { run_rake_task('import:github', 'token', user_name, project_path, github_repo) }
+
+ context 'when all inputs are correct' do
+ it 'imports a repository' do
+ expect_next_instance_of(Gitlab::GithubImport::SequentialImporter) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:after_import)
+ end
+
+ import_task
+ end
+ end
+
+ context 'when project path is invalid' do
+ let(:project_path) { target_namespace }
+
+ it 'aborts with an error' do
+ expect { import_task }.to raise_error(SystemExit, 'Project path must be: namespace(s)/project_name')
+ end
+ end
+
+ context 'when user is not found' do
+ let(:user_name) { 'unknown_user' }
+
+ it 'aborts with an error' do
+ expect { import_task }.to raise_error("GitLab user #{user_name} not found. Please specify a valid username.")
+ end
+ end
+
+ context 'when github repo is not found' do
+ let(:github_repo) { 'github_user/unknown_repo' }
+
+ it 'aborts with an error' do
+ expect { import_task }.to raise_error('No repo found!')
+ end
+ end
+
+ context 'when namespace to import repo into does not exists' do
+ let(:target_namespace) { 'unknown_namespace_path' }
+
+ it 'aborts with an error' do
+ expect { import_task }.to raise_error('Namespace or group to import repository into does not exist.')
+ end
+ end
+ end
+
+ context 'when importing multiple projects' do
+ subject(:import_task) { run_rake_task('import:github', 'token', user_name, project_path) }
+
+ context 'when user enters github repo id that exists' do
+ before do
+ allow($stdin).to receive(:gets).and_return("1\n")
+ end
+
+ it 'imports a repository' do
+ expect_next_instance_of(Gitlab::GithubImport::SequentialImporter) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:after_import)
+ end
+
+ import_task
+ end
+ end
+
+ context 'when user enters github repo id that does not exists' do
+ before do
+ allow($stdin).to receive(:gets).and_return("2\n")
+ end
+
+ it 'aborts with an error' do
+ expect { import_task }.to raise_error('No repo found!')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tasks/migrate/schema_check_rake_spec.rb b/spec/tasks/migrate/schema_check_rake_spec.rb
index 1b60b63ad84..ede55f23ba8 100644
--- a/spec/tasks/migrate/schema_check_rake_spec.rb
+++ b/spec/tasks/migrate/schema_check_rake_spec.rb
@@ -5,6 +5,7 @@ require 'rake'
RSpec.describe 'schema_version_check rake task', :silence_stdout do
include StubENV
+ let(:valid_schema_version) { 20211004170422 }
before :all do
Rake.application.rake_require 'active_record/railties/databases'
@@ -15,8 +16,8 @@ RSpec.describe 'schema_version_check rake task', :silence_stdout do
end
before do
- allow(ActiveRecord::Migrator).to receive(:current_version).and_return(Gitlab::Database::MIN_SCHEMA_VERSION)
-
+ allow(ActiveRecord::Migrator).to receive(:current_version).and_return(valid_schema_version)
+ allow(Gitlab::Database).to receive(:read_minimum_migration_version).and_return(valid_schema_version)
# Ensure our check can re-run each time
Rake::Task[:schema_version_check].reenable
end
diff --git a/spec/tooling/danger/config_files_spec.rb b/spec/tooling/danger/config_files_spec.rb
index 88b327df63f..65edcabb817 100644
--- a/spec/tooling/danger/config_files_spec.rb
+++ b/spec/tooling/danger/config_files_spec.rb
@@ -13,7 +13,6 @@ RSpec.describe Tooling::Danger::ConfigFiles do
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
- let(:matching_line) { "+ introduced_by_url:" }
subject(:config_file) { fake_danger.new(helper: fake_helper) }
@@ -22,29 +21,42 @@ RSpec.describe Tooling::Danger::ConfigFiles do
end
describe '#add_suggestion_for_missing_introduced_by_url' do
- let(:file_lines) do
+ let(:file_diff) do
[
- "---",
- "name: about_some_new_flow",
- "introduced_by_url: #{url}",
- "rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355909",
- "milestone: '14.10'"
+ "+---",
+ "+name: about_some_new_flow",
+ "+introduced_by_url: #{url}",
+ "+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355909",
+ "+milestone: '14.10'"
]
end
+ let(:file_lines) do
+ file_diff.map { |line| line.delete_prefix('+') }
+ end
+
let(:filename) { 'config/feature_flags/new_ff.yml' }
+ let(:mr_url) { 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1' }
before do
allow(config_file.project_helper).to receive(:file_lines).and_return(file_lines)
allow(config_file.helper).to receive(:added_files).and_return([filename])
- allow(config_file.helper).to receive(:mr_web_url).and_return(url)
+ allow(config_file.helper).to receive(:changed_lines).with(filename).and_return(file_diff)
+ allow(config_file.helper).to receive(:mr_web_url).and_return(mr_url)
end
context 'when config file has an empty introduced_by_url line' do
let(:url) { '' }
it 'adds suggestions at the correct line' do
- expected_format = format(described_class::SUGGEST_INTRODUCED_BY_COMMENT, url: url)
+ template = <<~SUGGEST_COMMENT
+ ```suggestion
+ introduced_by_url: %<mr_url>s
+ ```
+ SUGGEST_COMMENT
+
+ expected_format = format(template, mr_url: mr_url)
+
expect(config_file).to receive(:markdown).with(expected_format, file: filename, line: 3)
config_file.add_suggestion_for_missing_introduced_by_url
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
index 0e9eda54510..4575d8ca981 100644
--- a/spec/tooling/danger/feature_flag_spec.rb
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -86,14 +86,20 @@ RSpec.describe Tooling::Danger::FeatureFlag do
describe described_class::Found do
let(:feature_flag_path) { 'config/feature_flags/development/entry.yml' }
let(:group) { 'group::source code' }
- let(:raw_yaml) do
- YAML.dump(
+ let(:yaml) do
+ {
'group' => group,
'default_enabled' => true,
- 'rollout_issue_url' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/1'
- )
+ 'rollout_issue_url' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/1',
+ 'introduced_by_url' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/2',
+ 'milestone' => '15.9',
+ 'type' => 'development',
+ 'name' => 'entry'
+ }
end
+ let(:raw_yaml) { YAML.dump(yaml) }
+
subject(:found) { described_class.new(feature_flag_path) }
before do
@@ -101,58 +107,34 @@ RSpec.describe Tooling::Danger::FeatureFlag do
expect(File).to receive(:read).with(feature_flag_path).and_return(raw_yaml)
end
- describe '#raw' do
- it 'returns the raw YAML' do
- expect(found.raw).to eq(raw_yaml)
- end
- end
-
- describe '#group' do
- it 'returns the group found in the YAML' do
- expect(found.group).to eq(group)
- end
- end
-
- describe '#default_enabled' do
- it 'returns the default_enabled found in the YAML' do
- expect(found.default_enabled).to eq(true)
+ described_class::ATTRIBUTES.each do |attribute|
+ describe "##{attribute}" do
+ it 'returns value from the YAML' do
+ expect(found.public_send(attribute)).to eq(yaml[attribute])
+ end
end
end
- describe '#rollout_issue_url' do
- it 'returns the rollout_issue_url found in the YAML' do
- expect(found.rollout_issue_url).to eq('https://gitlab.com/gitlab-org/gitlab/-/issues/1')
+ describe '#raw' do
+ it 'returns the raw YAML' do
+ expect(found.raw).to eq(raw_yaml)
end
end
describe '#group_match_mr_label?' do
- subject(:result) { found.group_match_mr_label?(mr_group_label) }
-
- context 'when MR labels match FF group' do
- let(:mr_group_label) { 'group::source code' }
-
- specify { expect(result).to eq(true) }
- end
-
- context 'when MR labels does not match FF group' do
- let(:mr_group_label) { 'group::authentication and authorization' }
-
- specify { expect(result).to eq(false) }
- end
-
context 'when group is nil' do
let(:group) { nil }
- context 'and MR has no group label' do
- let(:mr_group_label) { nil }
-
- specify { expect(result).to eq(true) }
+ it 'is true only if MR has no group label' do
+ expect(found.group_match_mr_label?(nil)).to eq true
+ expect(found.group_match_mr_label?('group::source code')).to eq false
end
+ end
- context 'and MR has a group label' do
- let(:mr_group_label) { 'group::source code' }
-
- specify { expect(result).to eq(false) }
+ context 'when group is not nil' do
+ it 'is true only if MR has the same group label' do
+ expect(found.group_match_mr_label?(group)).to eq true
+ expect(found.group_match_mr_label?('group::authentication and authorization')).to eq false
end
end
end
diff --git a/spec/tooling/danger/product_intelligence_spec.rb b/spec/tooling/danger/product_intelligence_spec.rb
index fab8b0c61fa..c4cd0e5bfb6 100644
--- a/spec/tooling/danger/product_intelligence_spec.rb
+++ b/spec/tooling/danger/product_intelligence_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
let(:has_product_intelligence_label) { true }
before do
- allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ allow(fake_helper).to receive(:changed_lines).and_return(changed_lines) if defined?(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)
@@ -175,4 +175,49 @@ RSpec.describe Tooling::Danger::ProductIntelligence do
end
end
end
+
+ describe '#check_usage_data_insertions!' do
+ context 'when usage_data.rb is modified' do
+ let(:modified_files) { ['lib/gitlab/usage_data.rb'] }
+
+ before do
+ allow(fake_helper).to receive(:changed_lines).with("lib/gitlab/usage_data.rb").and_return(changed_lines)
+ end
+
+ context 'and has insertions' do
+ let(:changed_lines) { ['+ ci_runners: count(::Ci::CiRunner),'] }
+
+ it 'produces warning' do
+ expect(product_intelligence).to receive(:warn).with(/usage_data\.rb has been deprecated/)
+
+ product_intelligence.check_usage_data_insertions!
+ end
+ end
+
+ context 'and changes are not insertions' do
+ let(:changed_lines) { ['- ci_runners: count(::Ci::CiRunner),'] }
+
+ it 'doesnt do anything' do
+ expect(product_intelligence).not_to receive(:warn)
+
+ product_intelligence.check_usage_data_insertions!
+ end
+ end
+ end
+
+ context 'when usage_data.rb is not modified' do
+ context 'and another file has insertions' do
+ let(:modified_files) { ['tooling/danger/product_intelligence.rb'] }
+
+ it 'doesnt do anything' do
+ expect(fake_helper).to receive(:changed_lines).with("lib/gitlab/usage_data.rb").and_return([])
+ allow(fake_helper).to receive(:changed_lines).with("tooling/danger/product_intelligence.rb").and_return(["+ Inserting"])
+
+ expect(product_intelligence).not_to receive(:warn)
+
+ product_intelligence.check_usage_data_insertions!
+ end
+ end
+ end
+ end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 669867ffb4f..48050649f54 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -48,8 +48,8 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'PROCESS.md' | [:docs]
'README.md' | [:docs]
- 'ee/doc/foo' | [:unknown]
- 'ee/README' | [:unknown]
+ 'ee/doc/foo' | [:none]
+ 'ee/README' | [:none]
'app/assets/foo' | [:frontend]
'app/views/foo' | [:frontend, :backend]
@@ -139,7 +139,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template]
'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template]
- 'ee/FOO_VERSION' | [:unknown]
+ 'ee/FOO_VERSION' | [:none]
'db/schema.rb' | [:database]
'db/structure.sql' | [:database]
@@ -170,8 +170,8 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'locale/gitlab.pot' | [:none]
- 'FOO' | [:unknown]
- 'foo' | [:unknown]
+ 'FOO' | [:none]
+ 'foo' | [:none]
'foo/bar.rb' | [:backend]
'foo/bar.js' | [:frontend]
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index 422923827a8..cdac5954f92 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -19,14 +19,16 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
let(:file_lines) do
[
" describe 'foo' do",
- " expect(foo).to match(['bar'])",
+ " expect(foo).to match(['bar', 'baz'])",
" end",
- " expect(foo).to match(['bar'])", # same line as line 1 above, we expect two different suggestions
+ " expect(foo).to match(['bar', 'baz'])", # same line as line 1 above, we expect two different suggestions
" ",
- " expect(foo).to match ['bar']",
- " expect(foo).to eq(['bar'])",
- " expect(foo).to eq ['bar']",
- " expect(foo).to(match(['bar']))",
+ " expect(foo).to match ['bar', 'baz']",
+ " expect(foo).to eq(['bar', 'baz'])",
+ " expect(foo).to eq ['bar', 'baz']",
+ " expect(foo).to(match(['bar', 'baz']))",
+ " expect(foo).to(eq(['bar', 'baz']))",
+ " expect(foo).to(eq([bar, baz]))",
" expect(foo).to(eq(['bar']))",
" foo.eq(['bar'])"
]
@@ -35,28 +37,30 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
let(:matching_lines) do
[
"+ expect(foo).to match(['should not error'])",
- "+ expect(foo).to match(['bar'])",
- "+ expect(foo).to match(['bar'])",
- "+ expect(foo).to match ['bar']",
- "+ expect(foo).to eq(['bar'])",
- "+ expect(foo).to eq ['bar']",
- "+ expect(foo).to(match(['bar']))",
- "+ expect(foo).to(eq(['bar']))"
+ "+ expect(foo).to match(['bar', 'baz'])",
+ "+ expect(foo).to match(['bar', 'baz'])",
+ "+ expect(foo).to match ['bar', 'baz']",
+ "+ expect(foo).to eq(['bar', 'baz'])",
+ "+ expect(foo).to eq ['bar', 'baz']",
+ "+ expect(foo).to(match(['bar', 'baz']))",
+ "+ expect(foo).to(eq(['bar', 'baz']))",
+ "+ expect(foo).to(eq([bar, baz]))"
]
end
let(:changed_lines) do
[
- " expect(foo).to match(['bar'])",
- " expect(foo).to match(['bar'])",
- " expect(foo).to match ['bar']",
- " expect(foo).to eq(['bar'])",
- " expect(foo).to eq ['bar']",
- "- expect(foo).to match(['bar'])",
- "- expect(foo).to match(['bar'])",
- "- expect(foo).to match ['bar']",
- "- expect(foo).to eq(['bar'])",
- "- expect(foo).to eq ['bar']",
+ " expect(foo).to match(['bar', 'baz'])",
+ " expect(foo).to match(['bar', 'baz'])",
+ " expect(foo).to match ['bar', 'baz']",
+ " expect(foo).to eq(['bar', 'baz'])",
+ " expect(foo).to eq ['bar', 'baz']",
+ "- expect(foo).to match(['bar', 'baz'])",
+ "- expect(foo).to match(['bar', 'baz'])",
+ "- expect(foo).to match ['bar', 'baz']",
+ "- expect(foo).to eq(['bar', 'baz'])",
+ "- expect(foo).to eq ['bar', 'baz']",
+ "- expect(foo).to eq [bar, foo]",
"+ expect(foo).to eq([])"
] + matching_lines
end
@@ -107,7 +111,7 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
describe '#add_suggestions_for_match_with_array' do
let(:template) do
- <<~MARKDOWN
+ <<~MARKDOWN.chomp
```suggestion
%<suggested_line>s
```
@@ -118,13 +122,14 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
it 'adds suggestions at the correct lines' do
[
- { suggested_line: " expect(foo).to match_array(['bar'])", number: 2 },
- { suggested_line: " expect(foo).to match_array(['bar'])", number: 4 },
- { suggested_line: " expect(foo).to match_array ['bar']", number: 6 },
- { suggested_line: " expect(foo).to match_array(['bar'])", number: 7 },
- { suggested_line: " expect(foo).to match_array ['bar']", number: 8 },
- { suggested_line: " expect(foo).to(match_array(['bar']))", number: 9 },
- { suggested_line: " expect(foo).to(match_array(['bar']))", number: 10 }
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 2 },
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 4 },
+ { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 6 },
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 7 },
+ { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 8 },
+ { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 9 },
+ { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 10 },
+ { suggested_line: " expect(foo).to(match_array([bar, baz]))", number: 11 }
].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])
@@ -136,7 +141,7 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
describe '#add_suggestions_for_project_factory_usage' do
let(:template) do
- <<~MARKDOWN
+ <<~MARKDOWN.chomp
```suggestion
%<suggested_line>s
```
@@ -220,7 +225,7 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
describe '#add_suggestions_for_feature_category' do
let(:template) do
- <<~SUGGESTION_MARKDOWN
+ <<~SUGGESTION_MARKDOWN.chomp
```suggestion
%<suggested_line>s
```
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
index 08fd25b30e0..9eee077d493 100644
--- a/spec/tooling/danger/stable_branch_spec.rb
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
include_context 'with dangerfile'
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:fake_api) { double('Api') } # rubocop:disable RSpec/VerifiedDoubles
+ let(:gitlab_gem_client) { double('gitlab') } # rubocop:disable RSpec/VerifiedDoubles
let(:stable_branch) { fake_danger.new(helper: fake_helper) }
@@ -34,6 +36,28 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
end
end
+ shared_examples 'with a warning' do |warning_message|
+ it 'warns' do
+ expect(stable_branch).to receive(:warn).with(warning_message)
+
+ subject
+ end
+ end
+
+ shared_examples 'bypassing when flaky test or docs only' do
+ context 'when failure::flaky-test label is present' do
+ let(:flaky_test_label_present) { true }
+
+ it_behaves_like 'without a failure'
+ end
+
+ context 'with only docs changes' do
+ let(:changes_by_category_response) { { docs: ['foo.md'] } }
+
+ it_behaves_like 'without a failure'
+ end
+ end
+
context 'when not applicable' do
where(:stable_branch?, :security_mr?) do
true | true
@@ -47,15 +71,32 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
allow(fake_helper).to receive(:security_mr?).and_return(security_mr?)
end
- it_behaves_like "without a failure"
+ it_behaves_like 'without a failure'
end
end
context 'when applicable' do
let(:target_branch) { '15-1-stable-ee' }
+ let(:source_branch) { 'my_bug_branch' }
let(:feature_label_present) { false }
let(:bug_label_present) { true }
+ let(:pipeline_expedite_label_present) { false }
+ let(:flaky_test_label_present) { false }
let(:response_success) { true }
+
+ let(:changes_by_category_response) do
+ {
+ graphql: ['bar.rb']
+ }
+ end
+
+ let(:pipeline_bridges_response) do
+ [
+ { 'name' => 'e2e:package-and-test',
+ 'status' => 'success' }
+ ]
+ end
+
let(:parsed_response) do
[
{ 'version' => '15.1.1' },
@@ -79,14 +120,27 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
before do
allow(fake_helper).to receive(:mr_target_branch).and_return(target_branch)
+ allow(fake_helper).to receive(:mr_source_branch).and_return(source_branch)
allow(fake_helper).to receive(:security_mr?).and_return(false)
+ allow(fake_helper).to receive(:mr_target_project_id).and_return(1)
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(fake_helper).to receive(:mr_has_labels?).with('pipeline:expedite')
+ .and_return(pipeline_expedite_label_present)
+ allow(fake_helper).to receive(:mr_has_labels?).with('failure::flaky-test')
+ .and_return(flaky_test_label_present)
+ allow(fake_helper).to receive(:changes_by_category).and_return(changes_by_category_response)
allow(HTTParty).to receive(:get).with(/page=1/).and_return(version_response)
+ allow(fake_helper).to receive(:api).and_return(fake_api)
+ allow(stable_branch).to receive(:gitlab).and_return(gitlab_gem_client)
+ allow(gitlab_gem_client).to receive(:mr_json).and_return({ 'head_pipeline' => { 'id' => '1' } })
+ allow(gitlab_gem_client).to receive(:api).and_return(fake_api)
+ allow(fake_api).to receive(:pipeline_bridges).with(1, '1')
+ .and_return(pipeline_bridges_response)
end
# the stubbed behavior above is the success path
- it_behaves_like "without a failure"
+ it_behaves_like 'without a failure'
context 'with a feature label' do
let(:feature_label_present) { true }
@@ -100,20 +154,65 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
it_behaves_like 'with a failure', described_class::BUG_ERROR_MESSAGE
end
+ context 'with a pipeline:expedite label' do
+ let(:pipeline_expedite_label_present) { true }
+
+ it_behaves_like 'with a failure', described_class::PIPELINE_EXPEDITE_ERROR_MESSAGE
+ it_behaves_like 'bypassing when flaky test or docs only'
+ end
+
+ context 'when no package-and-test job is found' do
+ let(:pipeline_bridges_response) { nil }
+
+ it_behaves_like 'with a failure', described_class::NEEDS_PACKAGE_AND_TEST_MESSAGE
+ it_behaves_like 'bypassing when flaky test or docs only'
+ end
+
+ context 'when package-and-test job is in manual state' do
+ described_class::FAILING_PACKAGE_AND_TEST_STATUSES.each do |status|
+ let(:pipeline_bridges_response) do
+ [
+ { 'name' => 'e2e:package-and-test',
+ 'status' => status }
+ ]
+ end
+
+ it_behaves_like 'with a failure', described_class::NEEDS_PACKAGE_AND_TEST_MESSAGE
+ it_behaves_like 'bypassing when flaky test or docs only'
+ end
+ end
+
+ context 'when package-and-test job is in a non-successful state' do
+ let(:pipeline_bridges_response) do
+ [
+ { 'name' => 'e2e:package-and-test',
+ 'status' => 'running' }
+ ]
+ end
+
+ it_behaves_like 'with a warning', described_class::WARN_PACKAGE_AND_TEST_MESSAGE
+ it_behaves_like 'bypassing when flaky test or docs only'
+ end
+
+ context 'when no pipeline is found' do
+ before do
+ allow(gitlab_gem_client).to receive(:mr_json).and_return({})
+ end
+
+ it_behaves_like 'with a failure', described_class::NEEDS_PACKAGE_AND_TEST_MESSAGE
+ it_behaves_like 'bypassing when flaky test or docs only'
+ 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
+ it_behaves_like 'with a warning', described_class::VERSION_WARNING_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
+ it_behaves_like 'with a warning', described_class::FAILED_VERSION_REQUEST_MESSAGE
end
context 'when more than one page of versions is needed' do
@@ -151,7 +250,7 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
allow(HTTParty).to receive(:get).with(/page=2/).and_return(second_version_response)
end
- it_behaves_like "without a failure"
+ it_behaves_like 'without a failure'
end
context 'when too many version API requests are made' do
@@ -166,4 +265,24 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
end
end
end
+
+ describe '#non_security_stable_branch?' do
+ subject { stable_branch.non_security_stable_branch? }
+
+ where(:stable_branch?, :security_mr?, :expected_result) do
+ true | true | false
+ false | true | false
+ true | false | true
+ false | 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 { is_expected.to eq(expected_result) }
+ end
+ end
end
diff --git a/spec/tooling/lib/tooling/helm3_client_spec.rb b/spec/tooling/lib/tooling/helm3_client_spec.rb
index 52d1b5a1567..5a015ddfa7c 100644
--- a/spec/tooling/lib/tooling/helm3_client_spec.rb
+++ b/spec/tooling/lib/tooling/helm3_client_spec.rb
@@ -3,15 +3,14 @@
require_relative '../../../../tooling/lib/tooling/helm3_client'
RSpec.describe Tooling::Helm3Client do
- let(:namespace) { 'review-apps' }
let(:release_name) { 'my-release' }
let(:raw_helm_list_page1) do
<<~OUTPUT
[
- {"name":"review-qa-60-reor-1mugd1","namespace":"#{namespace}","revision":1,"updated":"2020-04-03 17:27:10.245952 +0800 +08","status":"failed","chart":"gitlab-1.1.3","app_version":"12.9.2"},
- {"name":"review-7846-fix-s-261vd6","namespace":"#{namespace}","revision":2,"updated":"2020-04-02 17:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.2"},
- {"name":"review-7867-snowp-lzo3iy","namespace":"#{namespace}","revision":1,"updated":"2020-04-02 15:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.1"},
- {"name":"review-6709-group-2pzeec","namespace":"#{namespace}","revision":2,"updated":"2020-04-01 21:27:12.245952 +0800 +08","status":"failed","chart":"gitlab-1.1.3","app_version":"12.9.1"}
+ {"name":"review-qa-60-reor-1mugd1","namespace":"review-qa-60-reor-1mugd1","revision":1,"updated":"2020-04-03 17:27:10.245952 +0800 +08","status":"failed","chart":"gitlab-1.1.3","app_version":"12.9.2"},
+ {"name":"review-7846-fix-s-261vd6","namespace":"review-7846-fix-s-261vd6","revision":2,"updated":"2020-04-02 17:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.2"},
+ {"name":"review-7867-snowp-lzo3iy","namespace":"review-7867-snowp-lzo3iy","revision":1,"updated":"2020-04-02 15:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.1"},
+ {"name":"review-6709-group-2pzeec","namespace":"review-6709-group-2pzeec","revision":2,"updated":"2020-04-01 21:27:12.245952 +0800 +08","status":"failed","chart":"gitlab-1.1.3","app_version":"12.9.1"}
]
OUTPUT
end
@@ -19,7 +18,7 @@ RSpec.describe Tooling::Helm3Client do
let(:raw_helm_list_page2) do
<<~OUTPUT
[
- {"name":"review-6709-group-t40qbv","namespace":"#{namespace}","revision":2,"updated":"2020-04-01 11:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.1"}
+ {"name":"review-6709-group-t40qbv","namespace":"review-6709-group-t40qbv","revision":2,"updated":"2020-04-01 11:27:12.245952 +0800 +08","status":"deployed","chart":"gitlab-1.1.3","app_version":"12.9.1"}
]
OUTPUT
end
@@ -30,7 +29,7 @@ RSpec.describe Tooling::Helm3Client do
OUTPUT
end
- subject { described_class.new(namespace: namespace) }
+ subject { described_class.new }
describe '#releases' do
it 'raises an error if the Helm command fails' do
@@ -74,7 +73,7 @@ RSpec.describe Tooling::Helm3Client do
status: 'deployed',
chart: 'gitlab-1.1.3',
app_version: '12.9.1',
- namespace: namespace
+ namespace: 'review-6709-group-t40qbv'
)
end
@@ -98,18 +97,19 @@ RSpec.describe Tooling::Helm3Client do
describe '#delete' do
it 'raises an error if the Helm command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall #{release_name})])
+ .with([%(helm uninstall --namespace #{release_name} #{release_name})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
- expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
+ expect { subject.delete(release_name: release_name, namespace: release_name) }
+ .to raise_error(described_class::CommandFailedError)
end
it 'calls helm uninstall with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall #{release_name})])
+ .with([%(helm uninstall --namespace #{release_name} #{release_name})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
- expect(subject.delete(release_name: release_name)).to eq('')
+ subject.delete(release_name: release_name, namespace: release_name)
end
context 'with multiple release names' do
@@ -117,18 +117,30 @@ RSpec.describe Tooling::Helm3Client do
it 'raises an error if the Helm command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall #{release_name.join(' ')})])
+ .with([%(helm uninstall --namespace #{release_name[0]} #{release_name[0]})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
end
- it 'calls helm uninstall with multiple release names' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall #{release_name.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ it 'calls helm uninstall with multiple release names and a namespace' do
+ release_name.each do |release|
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(helm uninstall --namespace namespace #{release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ end
+
+ subject.delete(release_name: release_name, namespace: 'namespace')
+ end
+
+ it 'calls helm uninstall with multiple release names and no namespace' do
+ release_name.each do |release|
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(helm uninstall --namespace #{release} #{release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ end
- expect(subject.delete(release_name: release_name)).to eq('')
+ subject.delete(release_name: release_name)
end
end
end
diff --git a/spec/tooling/lib/tooling/kubernetes_client_spec.rb b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
index a7f50b0bb50..50d33182a42 100644
--- a/spec/tooling/lib/tooling/kubernetes_client_spec.rb
+++ b/spec/tooling/lib/tooling/kubernetes_client_spec.rb
@@ -250,7 +250,7 @@ RSpec.describe Tooling::KubernetesClient do
describe '#review_app_namespaces_created_before' do
let(:three_days_ago) { Time.now - 3600 * 24 * 3 }
let(:two_days_ago) { Time.now - 3600 * 24 * 2 }
- let(:namespace_created_three_days_ago) { 'namespace-created-three-days-ago' }
+ let(:namespace_created_three_days_ago) { 'review-ns-created-three-days-ago' }
let(:resource_type) { 'namespace' }
let(:raw_resources) do
{
@@ -260,10 +260,7 @@ RSpec.describe Tooling::KubernetesClient do
kind: "Namespace",
metadata: {
creationTimestamp: three_days_ago,
- name: namespace_created_three_days_ago,
- labels: {
- tls: 'review-apps-tls'
- }
+ name: namespace_created_three_days_ago
}
},
{
@@ -271,10 +268,7 @@ RSpec.describe Tooling::KubernetesClient do
kind: "Namespace",
metadata: {
creationTimestamp: Time.now,
- name: 'another-pvc',
- labels: {
- tls: 'review-apps-tls'
- }
+ name: 'another-namespace'
}
}
]
@@ -283,12 +277,10 @@ RSpec.describe Tooling::KubernetesClient do
specify do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with(["kubectl get namespace " \
- "-l tls=review-apps-tls " \
- "--sort-by='{.metadata.creationTimestamp}' -o json"])
- .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true)))
+ .with(["kubectl get namespace --sort-by='{.metadata.creationTimestamp}' -o json"])
+ .and_return(Gitlab::Popen::Result.new([], raw_resources, '', double(success?: true)))
- expect(subject.__send__(:review_app_namespaces_created_before, created_before: two_days_ago)).to contain_exactly(namespace_created_three_days_ago)
+ expect(subject.__send__(:review_app_namespaces_created_before, created_before: two_days_ago)).to eq([namespace_created_three_days_ago])
end
end
end
diff --git a/spec/tooling/lib/tooling/mappings/base_spec.rb b/spec/tooling/lib/tooling/mappings/base_spec.rb
new file mode 100644
index 00000000000..935f833fa8b
--- /dev/null
+++ b/spec/tooling/lib/tooling/mappings/base_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative '../../../../../tooling/lib/tooling/mappings/view_to_js_mappings'
+
+RSpec.describe Tooling::Mappings::Base, feature_category: :tooling do
+ describe '#folders_for_available_editions' do
+ let(:base_folder_path) { 'app/views' }
+
+ subject { described_class.new.folders_for_available_editions(base_folder_path) }
+
+ context 'when FOSS' do
+ before do
+ allow(GitlabEdition).to receive(:ee?).and_return(false)
+ allow(GitlabEdition).to receive(:jh?).and_return(false)
+ end
+
+ it 'returns the correct paths' do
+ expect(subject).to match_array([base_folder_path])
+ end
+ end
+
+ context 'when EE' do
+ before do
+ allow(GitlabEdition).to receive(:ee?).and_return(true)
+ allow(GitlabEdition).to receive(:jh?).and_return(false)
+ end
+
+ it 'returns the correct paths' do
+ expect(subject).to match_array([base_folder_path, "ee/#{base_folder_path}"])
+ end
+ end
+
+ context 'when JiHu' do
+ before do
+ allow(GitlabEdition).to receive(:ee?).and_return(true)
+ allow(GitlabEdition).to receive(:jh?).and_return(true)
+ end
+
+ it 'returns the correct paths' do
+ expect(subject).to match_array([base_folder_path, "ee/#{base_folder_path}", "jh/#{base_folder_path}"])
+ end
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
new file mode 100644
index 00000000000..72e02547938
--- /dev/null
+++ b/spec/tooling/lib/tooling/mappings/js_to_system_specs_mappings_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require 'tempfile'
+require_relative '../../../../../tooling/lib/tooling/mappings/js_to_system_specs_mappings'
+
+RSpec.describe Tooling::Mappings::JsToSystemSpecsMappings, feature_category: :tooling do
+ # We set temporary folders, and those readers give access to those folder paths
+ attr_accessor :js_base_folder, :system_specs_base_folder
+
+ around do |example|
+ Dir.mktmpdir do |tmp_js_base_folder|
+ Dir.mktmpdir do |tmp_system_specs_base_folder|
+ self.system_specs_base_folder = tmp_system_specs_base_folder
+ self.js_base_folder = tmp_js_base_folder
+
+ example.run
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:instance) do
+ described_class.new(
+ system_specs_base_folder: system_specs_base_folder,
+ js_base_folder: js_base_folder
+ )
+ end
+
+ subject { instance.execute(changed_files) }
+
+ context 'when no JS files were changed' do
+ let(:changed_files) { [] }
+
+ it 'returns nothing' do
+ expect(subject).to match_array([])
+ end
+ end
+
+ context 'when some JS files were changed' do
+ let(:changed_files) { ["#{js_base_folder}/issues/secret_values.js"] }
+
+ context 'when the JS files are not present on disk' do
+ it 'returns nothing' do
+ expect(subject).to match_array([])
+ end
+ end
+
+ context 'when the JS files are present on disk' do
+ before do
+ FileUtils.mkdir_p("#{js_base_folder}/issues")
+ File.write("#{js_base_folder}/issues/secret_values.js", "hello")
+ end
+
+ context 'when no system specs match the JS keyword' do
+ it 'returns nothing' do
+ expect(subject).to match_array([])
+ end
+ end
+
+ context 'when a system spec matches the JS keyword' do
+ before do
+ FileUtils.mkdir_p("#{system_specs_base_folder}/confidential_issues")
+ File.write("#{system_specs_base_folder}/confidential_issues/issues_spec.rb", "a test")
+ end
+
+ it 'returns something' do
+ expect(subject).to match_array(["#{system_specs_base_folder}/confidential_issues/issues_spec.rb"])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#filter_files' do
+ subject { described_class.new(js_base_folder: js_base_folder).filter_files(changed_files) }
+
+ before do
+ File.write("#{js_base_folder}/index.js", "index.js")
+ File.write("#{js_base_folder}/index-with-ee-in-it.js", "index-with-ee-in-it.js")
+ File.write("#{js_base_folder}/index-with-jh-in-it.js", "index-with-jh-in-it.js")
+ end
+
+ context 'when no files were changed' do
+ let(:changed_files) { [] }
+
+ it 'returns an empty array' do
+ expect(subject).to match_array([])
+ end
+ end
+
+ context 'when JS files were changed' do
+ let(:changed_files) do
+ [
+ "#{js_base_folder}/index.js",
+ "#{js_base_folder}/index-with-ee-in-it.js",
+ "#{js_base_folder}/index-with-jh-in-it.js"
+ ]
+ end
+
+ it 'returns the path to the JS files' do
+ # "nil" group represents FOSS JS files in app/assets/javascripts
+ expect(subject).to match(nil => [
+ "#{js_base_folder}/index.js",
+ "#{js_base_folder}/index-with-ee-in-it.js",
+ "#{js_base_folder}/index-with-jh-in-it.js"
+ ])
+ end
+ end
+
+ context 'when JS files are deleted' do
+ let(:changed_files) { ["#{system_specs_base_folder}/deleted.html"] }
+
+ it 'returns an empty array' do
+ expect(subject).to match_array([])
+ end
+ end
+ end
+
+ describe '#construct_js_keywords' do
+ subject { described_class.new.construct_js_keywords(js_files) }
+
+ let(:js_files) do
+ %w[
+ app/assets/javascripts/boards/issue_board_filters.js
+ ee/app/assets/javascripts/queries/epic_due_date.query.graphql
+ ]
+ end
+
+ it 'returns a singularized keyword based on the first folder the file is in' do
+ expect(subject).to eq(%w[board query])
+ end
+ end
+
+ describe '#system_specs_for_edition' do
+ subject do
+ described_class.new(system_specs_base_folder: system_specs_base_folder).system_specs_for_edition(edition)
+ end
+
+ context 'when FOSS' do
+ let(:edition) { nil }
+
+ it 'checks the correct folder' do
+ expect(Dir).to receive(:[]).with("#{system_specs_base_folder}/**/*").and_call_original
+
+ subject
+ end
+ end
+
+ context 'when EE' do
+ let(:edition) { 'ee' }
+
+ it 'checks the correct folder' do
+ expect(Dir).to receive(:[]).with("ee#{system_specs_base_folder}/**/*").and_call_original
+
+ subject
+ end
+ end
+
+ context 'when JiHu' do
+ let(:edition) { 'jh' }
+
+ it 'checks the correct folder' do
+ expect(Dir).to receive(:[]).with("jh#{system_specs_base_folder}/**/*").and_call_original
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/view_to_js_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb
index b09df2a9200..eaa0124370d 100644
--- a/spec/tooling/lib/tooling/view_to_js_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/view_to_js_mappings_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'tempfile'
-require_relative '../../../../tooling/lib/tooling/view_to_js_mappings'
+require_relative '../../../../../tooling/lib/tooling/mappings/view_to_js_mappings'
-RSpec.describe Tooling::ViewToJsMappings, feature_category: :tooling do
+RSpec.describe Tooling::Mappings::ViewToJsMappings, feature_category: :tooling do
# We set temporary folders, and those readers give access to those folder paths
attr_accessor :view_base_folder, :js_base_folder
@@ -32,7 +32,7 @@ RSpec.describe Tooling::ViewToJsMappings, feature_category: :tooling do
context 'when no view files have been changed' do
before do
- allow(instance).to receive(:view_files).and_return([])
+ allow(instance).to receive(:filter_files).and_return([])
end
it 'returns nothing' do
@@ -140,8 +140,8 @@ RSpec.describe Tooling::ViewToJsMappings, feature_category: :tooling do
end
end
- describe '#view_files' do
- subject { described_class.new(view_base_folder: view_base_folder).view_files(changed_files) }
+ describe '#filter_files' do
+ subject { described_class.new(view_base_folder: view_base_folder).filter_files(changed_files) }
before do
File.write("#{js_base_folder}/index.js", "index.js")
@@ -181,45 +181,6 @@ RSpec.describe Tooling::ViewToJsMappings, feature_category: :tooling do
end
end
- describe '#folders_for_available_editions' do
- let(:base_folder_path) { 'app/views' }
-
- subject { described_class.new.folders_for_available_editions(base_folder_path) }
-
- context 'when FOSS' do
- before do
- allow(GitlabEdition).to receive(:ee?).and_return(false)
- allow(GitlabEdition).to receive(:jh?).and_return(false)
- end
-
- it 'returns the correct paths' do
- expect(subject).to match_array([base_folder_path])
- end
- end
-
- context 'when EE' do
- before do
- allow(GitlabEdition).to receive(:ee?).and_return(true)
- allow(GitlabEdition).to receive(:jh?).and_return(false)
- end
-
- it 'returns the correct paths' do
- expect(subject).to eq([base_folder_path, "ee/#{base_folder_path}"])
- end
- end
-
- context 'when JiHu' do
- before do
- allow(GitlabEdition).to receive(:ee?).and_return(true)
- allow(GitlabEdition).to receive(:jh?).and_return(true)
- end
-
- it 'returns the correct paths' do
- expect(subject).to eq([base_folder_path, "ee/#{base_folder_path}", "jh/#{base_folder_path}"])
- end
- end
- end
-
describe '#find_partials' do
subject { described_class.new(view_base_folder: view_base_folder).find_partials(file_path) }
diff --git a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
index 96755b7292b..184c664f6dc 100644
--- a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
@@ -3,7 +3,10 @@
require 'spec_helper'
RSpec.describe ObjectStorage::CDN::GoogleCDN,
- :use_clean_rails_memory_store_caching, :use_clean_rails_redis_caching, :sidekiq_inline do
+ :use_clean_rails_memory_store_caching,
+ :use_clean_rails_redis_caching,
+ :sidekiq_inline,
+ feature_category: :build_artifacts do # the google cdn is currently only used by build artifacts
include StubRequests
let(:key) { SecureRandom.hex }
diff --git a/spec/uploaders/object_storage/cdn_spec.rb b/spec/uploaders/object_storage/cdn_spec.rb
index 2a447921a19..d6c638297fa 100644
--- a/spec/uploaders/object_storage/cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ObjectStorage::CDN do
+RSpec.describe ObjectStorage::CDN, feature_category: :build_artifacts do
let(:cdn_options) do
{
'object_store' => {
@@ -32,6 +32,7 @@ RSpec.describe ObjectStorage::CDN do
let(:object) { build_stubbed(:user) }
let(:public_ip) { '18.245.0.1' }
+ let(:query_params) { { foo: :bar } }
let_it_be(:project) { build(:project) }
@@ -46,9 +47,9 @@ RSpec.describe ObjectStorage::CDN do
describe '#cdn_enabled_url' do
it 'calls #cdn_signed_url' do
expect(subject).not_to receive(:url)
- expect(subject).to receive(:cdn_signed_url).and_call_original
+ expect(subject).to receive(:cdn_signed_url).with(query_params).and_call_original
- result = subject.cdn_enabled_url(public_ip)
+ result = subject.cdn_enabled_url(public_ip, query_params)
expect(result.used_cdn).to be true
end
@@ -76,6 +77,17 @@ RSpec.describe ObjectStorage::CDN do
uploader_class.options = Gitlab.config.uploads
end
+ describe '#cdn_enabled_url' do
+ it 'calls #url' do
+ expect(subject).not_to receive(:cdn_signed_url)
+ expect(subject).to receive(:url).with(query: query_params).and_call_original
+
+ result = subject.cdn_enabled_url(public_ip, query_params)
+
+ expect(result.used_cdn).to be false
+ end
+ end
+
describe '#use_cdn?' do
it 'returns false' do
expect(subject.use_cdn?(public_ip)).to be false
diff --git a/spec/uploaders/object_storage/s3_spec.rb b/spec/uploaders/object_storage/s3_spec.rb
new file mode 100644
index 00000000000..de86642c58d
--- /dev/null
+++ b/spec/uploaders/object_storage/s3_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ObjectStorage::S3, feature_category: :source_code_management do
+ describe '.signed_head_url' do
+ subject { described_class.signed_head_url(package_file.file) }
+
+ let(:package_file) { create(:package_file) }
+
+ context 'when the provider is AWS' do
+ before do
+ stub_lfs_object_storage(config: Gitlab.config.lfs.object_store.merge(
+ connection: {
+ provider: 'AWS',
+ aws_access_key_id: 'test',
+ aws_secret_access_key: 'test'
+ }
+ ))
+ end
+
+ it 'generates a signed url' do
+ expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
+ expect(instance).to receive(:head_url).and_return(a_valid_url)
+ end
+
+ subject
+ end
+
+ it 'delegates to Fog::AWS::Storage::Files#head_url' do
+ expect_next_instance_of(Fog::AWS::Storage::Files) do |instance|
+ expect(instance).to receive(:head_url).and_return('stubbed_url')
+ end
+
+ expect(subject).to eq('stubbed_url')
+ end
+ end
+ end
+end
diff --git a/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb b/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb
index 7cfc2db5a41..af411261aa0 100644
--- a/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'admin/application_settings/_jira_connect.html.haml' do
+RSpec.describe 'admin/application_settings/_jira_connect.html.haml', feature_category: :integrations do
let_it_be(:admin) { create(:admin) }
let(:application_setting) { build(:application_setting) }
@@ -20,4 +20,9 @@ RSpec.describe 'admin/application_settings/_jira_connect.html.haml' do
render
expect(rendered).to have_field('Jira Connect Proxy URL', type: 'text')
end
+
+ it 'renders the enable public key storage checkbox' do
+ render
+ expect(rendered).to have_field('Enable public key storage', type: 'checkbox')
+ 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 e4ebdd706d4..5ef9399487f 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
@@ -25,7 +25,7 @@ RSpec.describe 'admin/application_settings/ci_cd.html.haml' do
render
expect(rendered).to have_content("Runner registration")
- expect(rendered).to have_content("If no options are selected, only administrators can register runners.")
+ expect(rendered).to have_content(s_("Runners|If both settings are disabled, new runners cannot be registered."))
end
end
end
diff --git a/spec/views/groups/group_members/index.html.haml_spec.rb b/spec/views/groups/group_members/index.html.haml_spec.rb
index c7aebb94a45..0b3b149238f 100644
--- a/spec/views/groups/group_members/index.html.haml_spec.rb
+++ b/spec/views/groups/group_members/index.html.haml_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe 'groups/group_members/index', :aggregate_failures do
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group) }
+RSpec.describe 'groups/group_members/index', :aggregate_failures, feature_category: :subgroups do
+ let_it_be(:user) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
before do
allow(view).to receive(:group_members_app_data).and_return({})
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
index 30c27078ad8..527ba1498b9 100644
--- a/spec/views/layouts/application.html.haml_spec.rb
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'layouts/application', :themed_layout do
+RSpec.describe 'layouts/application' do
let(:user) { create(:user) }
before do
@@ -14,6 +14,8 @@ RSpec.describe 'layouts/application', :themed_layout do
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
end
+ it_behaves_like 'a layout which reflects the application theme setting'
+
describe "visual review toolbar" do
context "ENV['REVIEW_APPS_ENABLED'] is set to true" do
before do
diff --git a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
index 4f1dcf54216..a027bdd6357 100644
--- a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
+++ b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
@@ -29,8 +29,7 @@ RSpec.describe 'layouts/header/_gitlab_version' do
)
expect(rendered).to have_selector(
- 'a[data-testid="gitlab-version-container"]' \
- "[data-track-property=\"#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}\"]"
+ 'a[data-testid="gitlab-version-container"][data-track-property="navigation_top"]'
)
end
end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 17251049c57..178448022d1 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'layouts/header/_new_dropdown' do
- let_it_be(:user) { create(:user) }
+RSpec.describe 'layouts/header/_new_dropdown', feature_category: :navigation do
+ let_it_be(:user) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
- shared_examples_for 'invite member quick link' do
+ shared_examples_for 'invite member selector' do
context 'with ability to invite members' do
it { is_expected.to have_link('Invite members', href: href) }
end
@@ -17,8 +17,8 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
end
- context 'group-specific links' do
- let_it_be(:group) { create(:group) }
+ context 'with group-specific links' do
+ let_it_be(:group) { create(:group) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
before do
stub_current_user(user)
@@ -40,21 +40,19 @@ RSpec.describe 'layouts/header/_new_dropdown' do
it 'has a "New subgroup" link' do
render
- expect(rendered).to have_link(
- 'New subgroup',
- href: new_group_path(parent_id: group.id, anchor: 'create-group-pane')
- )
+ expect(rendered)
+ .to have_link('New subgroup', href: new_group_path(parent_id: group.id, anchor: 'create-group-pane'))
end
end
- describe 'invite members quick link' do
+ describe 'invite members item' do
let(:href) { group_group_members_path(group) }
let(:invite_member) { true }
before do
allow(view).to receive(:can?).with(user, :create_projects, group).and_return(true)
allow(view).to receive(:can?).with(user, :admin_group_member, group).and_return(invite_member)
- allow(view).to receive(:can_admin_project_member?).and_return(invite_member)
+ allow(view).to receive(:can_admin_group_member?).and_return(invite_member)
end
subject do
@@ -63,12 +61,12 @@ RSpec.describe 'layouts/header/_new_dropdown' do
rendered
end
- it_behaves_like 'invite member quick link'
+ it_behaves_like 'invite member selector'
end
end
- context 'project-specific links' do
- let_it_be(:project) { create(:project, creator: user, namespace: user.namespace) }
+ context 'with project-specific links' do
+ let_it_be(:project) { create(:project, creator: user, namespace: user.namespace) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
before do
assign(:project, project)
@@ -99,7 +97,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
context 'as a Project guest' do
- let_it_be(:guest) { create(:user) }
+ let_it_be(:guest) { create(:user) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
before do
stub_current_user(guest)
@@ -119,14 +117,13 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
end
- describe 'invite members quick link' do
+ describe 'invite members item' do
let(:invite_member) { true }
let(:href) { project_project_members_path(project) }
before do
allow(view).to receive(:can_admin_project_member?).and_return(invite_member)
stub_current_user(user)
- allow(view).to receive(:experiment_enabled?)
end
subject do
@@ -135,11 +132,11 @@ RSpec.describe 'layouts/header/_new_dropdown' do
rendered
end
- it_behaves_like 'invite member quick link'
+ it_behaves_like 'invite member selector'
end
end
- context 'global links' do
+ context 'with global links' do
before do
stub_current_user(user)
end
@@ -163,7 +160,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
context 'when the user is not allowed to do anything' do
- let(:user) { create(:user, :external) }
+ let(:user) { create(:user, :external) } # rubocop:disable RSpec/FactoryBot/AvoidCreate
it 'is nil' do
# We have to use `view.render` because `render` causes issues
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 4de2c011b93..cddff276317 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'layouts/nav/sidebar/_project' do
+RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
@@ -67,19 +67,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
- describe 'Learn GitLab' do
- it 'has a link to the learn GitLab' do
- allow(view).to receive(:learn_gitlab_enabled?).and_return(true)
- allow_next_instance_of(Onboarding::Completion) do |onboarding|
- expect(onboarding).to receive(:percentage).and_return(20)
- end
-
- render
-
- expect(rendered).to have_link('Learn GitLab', href: project_learn_gitlab_path(project))
- end
- end
-
describe 'Repository' do
it 'has a link to the project tree path' do
render
@@ -96,24 +83,10 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Commits' do
- context 'when the use_ref_type_parameter flag is not enabled' do
- before do
- stub_feature_flags(use_ref_type_parameter: false)
- end
-
- 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
+ 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
+ expect(rendered).to have_link('Commits', href: project_commits_path(project, current_ref, ref_type: 'heads'), id: 'js-onboarding-commits-link')
end
end
@@ -134,24 +107,10 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Contributors' do
- context 'and the use_ref_type_parameter flag is disabled' do
- before do
- stub_feature_flags(use_ref_type_parameter: false)
- end
-
- 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
+ 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
+ expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref, ref_type: 'heads'))
end
end
diff --git a/spec/views/layouts/snippets.html.haml_spec.rb b/spec/views/layouts/snippets.html.haml_spec.rb
index 69378906bcd..1e6963a6526 100644
--- a/spec/views/layouts/snippets.html.haml_spec.rb
+++ b/spec/views/layouts/snippets.html.haml_spec.rb
@@ -2,41 +2,25 @@
require 'spec_helper'
-RSpec.describe 'layouts/snippets', feature_category: :snippets do
+RSpec.describe 'layouts/snippets', feature_category: :source_code_management do
before do
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
end
describe 'sidebar' do
- context 'when feature flag is on' do
- context 'when signed in' do
- let(:user) { build_stubbed(:user) }
-
- it 'renders the "Your work" sidebar' do
- render
-
- expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
- end
- end
-
- context 'when not signed in' do
- let(:user) { nil }
+ context 'when signed in' do
+ let(:user) { build_stubbed(:user) }
- it 'renders no sidebar' do
- render
+ it 'renders the "Your work" sidebar' do
+ render
- expect(rendered).not_to have_css('aside.nav-sidebar')
- end
+ expect(rendered).to have_css('aside.nav-sidebar[aria-label="Your work"]')
end
end
- context 'when feature flag is off' do
- before do
- stub_feature_flags(your_work_sidebar: false)
- end
-
- let(:user) { build_stubbed(:user) }
+ context 'when not signed in' do
+ let(:user) { nil }
it 'renders no sidebar' do
render
diff --git a/spec/views/notify/user_deactivated_email.html.haml_spec.rb b/spec/views/notify/user_deactivated_email.html.haml_spec.rb
new file mode 100644
index 00000000000..25d18e37cb9
--- /dev/null
+++ b/spec/views/notify/user_deactivated_email.html.haml_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'notify/user_deactivated_email.html.haml', feature_category: :user_management do
+ let(:name) { 'John Smith' }
+
+ before do
+ assign(:name, name)
+ end
+
+ it "displays the user's name" do
+ render
+
+ expect(rendered).to have_content(/^Hello John Smith,/)
+ end
+
+ context 'when additional text setting is set' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:deactivation_email_additional_text)
+ .and_return('So long and thanks for all the fish!')
+ end
+
+ context 'when additional text feature flag is enabled' do
+ it 'displays the additional text' do
+ render
+
+ expect(rendered).to have_content(/So long and thanks for all the fish!$/)
+ end
+ end
+
+ context 'when additional text feature flag is disabled' do
+ before do
+ stub_feature_flags(deactivation_email_additional_text: false)
+ end
+
+ it 'does not display the additional text' do
+ render
+
+ expect(rendered).to have_content(/Please contact your GitLab administrator if you think this is an error\.$/)
+ end
+ end
+ end
+
+ context 'when additional text setting is not set' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:deactivation_email_additional_text).and_return('')
+ end
+
+ it 'does not display any additional text' do
+ render
+
+ expect(rendered).to have_content(/Please contact your GitLab administrator if you think this is an error\.$/)
+ end
+ end
+end
diff --git a/spec/views/notify/user_deactivated_email.text.erb_spec.rb b/spec/views/notify/user_deactivated_email.text.erb_spec.rb
new file mode 100644
index 00000000000..8cf56816b92
--- /dev/null
+++ b/spec/views/notify/user_deactivated_email.text.erb_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'notify/user_deactivated_email.text.erb', feature_category: :user_management do
+ let(:name) { 'John Smith' }
+
+ before do
+ assign(:name, name)
+ end
+
+ it_behaves_like 'renders plain text email correctly'
+
+ it "displays the user's name" do
+ render
+
+ expect(rendered).to have_content(/^Hello John Smith,/)
+ end
+
+ context 'when additional text setting is set' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:deactivation_email_additional_text)
+ .and_return('So long and thanks for all the fish!')
+ end
+
+ context 'when additional text feature flag is enabled' do
+ it 'displays the additional text' do
+ render
+
+ expect(rendered).to have_content(/So long and thanks for all the fish!$/)
+ end
+ end
+
+ context 'when additional text feature flag is disabled' do
+ before do
+ stub_feature_flags(deactivation_email_additional_text: false)
+ end
+
+ it 'does not display the additional text' do
+ render
+
+ expect(rendered).to have_content(/Please contact your GitLab administrator if you think this is an error\.$/)
+ end
+ end
+ end
+
+ context 'when additional text setting is not set' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:deactivation_email_additional_text).and_return('')
+ end
+
+ it 'does not display any additional text' do
+ render
+
+ expect(rendered).to have_content(/Please contact your GitLab administrator if you think this is an error\.$/)
+ end
+ end
+end
diff --git a/spec/views/profiles/keys/_key.html.haml_spec.rb b/spec/views/profiles/keys/_key.html.haml_spec.rb
index d2e27bd2ee0..2ddbd3e6e14 100644
--- a/spec/views/profiles/keys/_key.html.haml_spec.rb
+++ b/spec/views/profiles/keys/_key.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'profiles/keys/_key.html.haml' do
+RSpec.describe 'profiles/keys/_key.html.haml', feature_category: :authentication_and_authorization do
let_it_be(:user) { create(:user) }
before do
@@ -24,18 +24,58 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
expect(rendered).to have_text(key.title)
expect(rendered).to have_css('[data-testid="key-icon"]')
expect(rendered).to have_text(key.fingerprint)
- expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
expect(rendered).to have_text(l(key.created_at, format: "%b %d, %Y"))
expect(rendered).to have_text(key.expires_at.to_date)
- expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete')
+ expect(rendered).to have_button('Remove')
+ end
+
+ context 'when disable_ssh_key_used_tracking is enabled' do
+ before do
+ stub_feature_flags(disable_ssh_key_used_tracking: true)
+ end
+
+ it 'renders "Unavailable" for last used' do
+ render
+
+ expect(rendered).to have_text('Last used: Unavailable')
+ end
+ end
+
+ context 'when disable_ssh_key_used_tracking is disabled' do
+ before do
+ stub_feature_flags(disable_ssh_key_used_tracking: false)
+ end
+
+ it 'displays the correct last used date' do
+ render
+
+ expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
+ end
+
+ context 'when the key has not been used' do
+ let_it_be(:key) do
+ create(:personal_key,
+ user: user,
+ last_used_at: nil)
+ end
+
+ it 'renders "Never" for last used' do
+ render
+
+ expect(rendered).to have_text('Last used: Never')
+ end
+ end
end
context 'displays the usage type' do
- where(:usage_type, :usage_type_text) do
+ where(:usage_type, :usage_type_text, :displayed_buttons, :hidden_buttons, :revoke_ssh_signatures_ff) do
[
- [:auth, 'Authentication'],
- [:auth_and_signing, 'Authentication & Signing'],
- [:signing, 'Signing']
+ [:auth, 'Authentication', ['Remove'], ['Revoke'], true],
+ [:auth_and_signing, 'Authentication & Signing', %w[Remove Revoke], [], true],
+ [:signing, 'Signing', %w[Remove Revoke], [], true],
+ [:auth, 'Authentication', ['Remove'], ['Revoke'], false],
+ [:auth_and_signing, 'Authentication & Signing', %w[Remove], ['Revoke'], false],
+ [:signing, 'Signing', %w[Remove], ['Revoke'], false]
]
end
@@ -47,20 +87,20 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
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,
- user: user,
- last_used_at: nil)
- end
+ it 'renders remove/revoke buttons', :aggregate_failures do
+ stub_feature_flags(revoke_ssh_signatures: revoke_ssh_signatures_ff)
- it 'renders "Never" for last used' do
- render
+ render
- expect(rendered).to have_text('Last used: Never')
+ displayed_buttons.each do |button|
+ expect(rendered).to have_text(button)
+ end
+
+ hidden_buttons.each do |button|
+ expect(rendered).not_to have_text(button)
+ end
+ end
end
end
@@ -98,7 +138,8 @@ RSpec.describe 'profiles/keys/_key.html.haml' do
it 'does not render the partial' do
render
- expect(response).not_to render_template(partial: 'shared/ssh_keys/_key_delete')
+ expect(response).not_to have_text('Remove')
+ expect(response).not_to have_text('Revoke')
end
end
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb
index 5751d47ee97..d5cb5694031 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe 'profiles/show' do
- let(:user) { create(:user) }
+ let_it_be(:user_status) { create(:user_status, clear_status_at: 8.hours.from_now) }
+ let_it_be(:user) { user_status.user }
before do
assign(:user, user)
@@ -23,5 +24,30 @@ RSpec.describe 'profiles/show' do
"rel=\"noopener noreferrer\">#{_('Learn more.')}</a>"
expect(rendered.include?(expected_link_html)).to eq(true)
end
+
+ it 'renders required hidden inputs for set status form' do
+ render
+
+ expect(rendered).to have_field(
+ 'user[status][emoji]',
+ with: user_status.emoji,
+ type: :hidden
+ )
+ expect(rendered).to have_field(
+ 'user[status][message]',
+ with: user_status.message,
+ type: :hidden
+ )
+ expect(rendered).to have_field(
+ 'user[status][availability]',
+ with: user_status.availability,
+ type: :hidden
+ )
+ expect(rendered).to have_field(
+ 'user[status][clear_status_after]',
+ with: user_status.clear_status_at.to_s(:iso8601),
+ type: :hidden
+ )
+ 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 52b3d5b95f9..eba54628215 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', feature_category: :source_code do
+RSpec.describe 'projects/commit/show.html.haml', feature_category: :source_code_management do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
@@ -79,7 +79,7 @@ RSpec.describe 'projects/commit/show.html.haml', feature_category: :source_code
context 'when commit is signed' do
let(:page) { Nokogiri::HTML.parse(rendered) }
- let(:badge) { page.at('.gpg-status-box') }
+ let(:badge) { page.at('.signature-badge') }
let(:badge_attributes) { badge.attributes }
let(:title) { badge_attributes['data-title'].value }
let(:content) { badge_attributes['data-content'].value }
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index 4e8a680b6de..90df0d381ed 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
commit: commit
}
- within '.gpg-status-box' do
+ within '.signature-badge' do
expect(page).not_to have_css('.gl-spinner')
end
end
diff --git a/spec/views/projects/issues/_issue.html.haml_spec.rb b/spec/views/projects/issues/_issue.html.haml_spec.rb
index 29bef557304..e4485f253b6 100644
--- a/spec/views/projects/issues/_issue.html.haml_spec.rb
+++ b/spec/views/projects/issues/_issue.html.haml_spec.rb
@@ -37,6 +37,59 @@ RSpec.describe 'projects/issues/_issue.html.haml' do
end
end
+ context 'when issue is service desk issue' do
+ let_it_be(:email) { 'user@example.com' }
+ let_it_be(:obfuscated_email) { 'us*****@e*****.c**' }
+ let_it_be(:issue) { create(:issue, author: User.support_bot, service_desk_reply_to: email) }
+
+ context 'with anonymous user' do
+ it 'obfuscates service_desk_reply_to email for anonymous user' do
+ expect(rendered).to have_content(obfuscated_email)
+ end
+ end
+
+ context 'with signed in user' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:issue).and_return(issue)
+ end
+
+ context 'when user has no role in project' do
+ it 'obfuscates service_desk_reply_to email' do
+ render
+
+ expect(rendered).to have_content(obfuscated_email)
+ end
+ end
+
+ context 'when user has guest role in project' do
+ before do
+ issue.project.add_guest(user)
+ end
+
+ it 'obfuscates service_desk_reply_to email' do
+ render
+
+ expect(rendered).to have_content(obfuscated_email)
+ end
+ end
+
+ context 'when user has (at least) reporter role in project' do
+ before do
+ issue.project.add_reporter(user)
+ end
+
+ it 'shows full service_desk_reply_to email' do
+ render
+
+ expect(rendered).to have_content(email)
+ end
+ end
+ end
+ end
+
def format_timestamp(time)
l(time, format: "%b %d, %Y")
end
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index 90ee6638142..4ce6755b89d 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -37,6 +37,6 @@ RSpec.describe 'projects/merge_requests/_commits.html.haml', :sidekiq_might_not_
it 'shows signature verification badge' do
render
- expect(rendered).to have_css('.gpg-status-box')
+ expect(rendered).to have_css('.js-loading-signature-badge')
end
end
diff --git a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
index feb82e6a2b2..38f4daf7feb 100644
--- a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'projects/merge_requests/creations/_new_submit.html.haml' do
+RSpec.describe 'projects/merge_requests/creations/_new_submit.html.haml', feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let!(:pipeline) { create(:ci_empty_pipeline) }
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 7886a811c9a..d604eb2c8d6 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 administrator')
+ expect(rendered).to have_selector('.js-report-abuse-dropdown-item')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -29,7 +29,8 @@ RSpec.describe 'projects/notes/_more_actions_dropdown' 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 administrator')
+ expect(rendered).to have_selector('.js-report-abuse-dropdown-item')
+
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/views/projects/project_members/index.html.haml_spec.rb b/spec/views/projects/project_members/index.html.haml_spec.rb
index 382d400b961..4c4cde01cca 100644
--- a/spec/views/projects/project_members/index.html.haml_spec.rb
+++ b/spec/views/projects/project_members/index.html.haml_spec.rb
@@ -2,9 +2,9 @@
require 'spec_helper'
-RSpec.describe 'projects/project_members/index', :aggregate_failures do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :empty_repo, :with_namespace_settings).present(current_user: user) }
+RSpec.describe 'projects/project_members/index', :aggregate_failures, feature_category: :projects do
+ let_it_be(:user) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
+ let_it_be(:project) { create(:project, :empty_repo, :with_namespace_settings).present(current_user: user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate
before do
allow(view).to receive(:project_members_app_data_json).and_return({})
diff --git a/spec/views/projects/runners/_specific_runners.html.haml_spec.rb b/spec/views/projects/runners/_project_runners.html.haml_spec.rb
index ce16e0d5ac6..8a7e693bdeb 100644
--- a/spec/views/projects/runners/_specific_runners.html.haml_spec.rb
+++ b/spec/views/projects/runners/_project_runners.html.haml_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'projects/runners/specific_runners.html.haml' do
+RSpec.describe 'projects/runners/_project_runners.html.haml', feature_category: :runner do
describe 'render' do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:user) { build(:user) }
+ let_it_be(:project) { build(:project) }
before do
@project = project
@@ -22,7 +22,7 @@ RSpec.describe 'projects/runners/specific_runners.html.haml' do
end
it 'enables the Remove project button for a project' do
- render 'projects/runners/specific_runners', project: project
+ render 'projects/runners/project_runners', project: project
expect(rendered).to have_selector '#js-install-runner'
expect(rendered).not_to have_content 'Please contact an admin to register runners.'
@@ -35,7 +35,7 @@ RSpec.describe 'projects/runners/specific_runners.html.haml' do
end
it 'does not enable the Remove project button for a project' do
- render 'projects/runners/specific_runners', project: project
+ render 'projects/runners/project_runners', project: project
expect(rendered).to have_content 'Please contact an admin to register runners.'
expect(rendered).not_to have_selector '#js-install-runner'
diff --git a/spec/views/search/_results.html.haml_spec.rb b/spec/views/search/_results.html.haml_spec.rb
index de994a0da2b..ed71a03c7e0 100644
--- a/spec/views/search/_results.html.haml_spec.rb
+++ b/spec/views/search/_results.html.haml_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'search/_results', feature_category: :global_search do
- using RSpec::Parameterized::TableSyntax
-
let_it_be(:user) { create(:user) }
let(:search_objects) { Issue.page(1).per(2) }
@@ -32,30 +30,22 @@ RSpec.describe 'search/_results', feature_category: :global_search do
assign(:search_service_presenter, search_service_presenter)
end
- where(search_page_vertical_nav_enabled: [true, false])
+ describe 'page size' do
+ context 'when search results have a count' do
+ it 'displays the page size' do
+ render
- 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
+ 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 }
+ 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
+ it 'does not display the page size' do
+ render
- expect(rendered).not_to have_content(/Showing .* of .*/)
- end
+ expect(rendered).not_to have_content(/Showing .* of .*/)
end
end
end
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 6adb2c77c4d..db06adfeb6b 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -10,122 +10,96 @@ RSpec.describe 'search/show', feature_category: :global_search do
end
before do
- stub_template "search/_category.html.haml" => 'Category Partial'
stub_template "search/_results.html.haml" => 'Results Partial'
+ allow(view).to receive(:current_user) { user }
+
assign(:search_service_presenter, search_service_presenter)
+ assign(:search_term, search_term)
end
- context 'search_page_vertical_nav feature flag enabled' do
- before do
- allow(view).to receive(:current_user) { user }
- assign(:search_term, search_term)
+ context 'when search term is supplied' do
+ let(:search_term) { 'Search Foo' }
+
+ it 'renders the results partial' do
+ render
+
+ expect(rendered).to render_template('search/_results')
end
+ end
- context 'when search term is supplied' do
- let(:search_term) { 'Search Foo' }
+ context 'when the search page is opened' do
+ it 'displays the title' do
+ render
- it 'will not render category partial' do
- render
+ expect(rendered).to have_selector('h1.page-title', text: 'Search')
+ expect(rendered).not_to have_selector('h1.page-title code')
+ end
- expect(rendered).not_to render_template('search/_category')
- expect(rendered).to render_template('search/_results')
- end
+ it 'does not render the results partial' do
+ render
+
+ expect(rendered).not_to render_template('search/_results')
end
end
- context 'search_page_vertical_nav feature flag disabled' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
+ context 'unfurling support' do
+ let(:group) { build(:group) }
+ let(:search_results) do
+ instance_double(Gitlab::GroupSearchResults).tap do |double|
+ allow(double).to receive(:formatted_count).and_return(0)
+ end
+ end
- assign(:search_term, search_term)
+ before do
+ assign(:search_results, search_results)
+ assign(:scope, 'issues')
+ assign(:group, group)
end
- context 'when the search page is opened' do
- it 'displays the title' do
+ context 'search with full count' do
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
+ end
+
+ it 'renders meta tags for a group' do
render
- expect(rendered).to have_selector('h1.page-title', text: 'Search')
- expect(rendered).not_to have_selector('h1.page-title code')
+ expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path)
end
- it 'does not render partials' do
+ it 'renders meta tags for both group and project' do
+ project = build(:project, group: group)
+ assign(:project, project)
+
render
- expect(rendered).not_to render_template('search/_category')
- expect(rendered).not_to render_template('search/_results')
+ expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path, "Project" => project.full_path)
end
end
- context 'when search term is supplied' do
- let(:search_term) { 'Search Foo' }
+ context 'search without full count' do
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: true, advanced_search_enabled?: false)
+ end
- it 'renders partials' do
+ it 'renders meta tags for a group' do
render
- expect(rendered).to render_template('search/_category')
- expect(rendered).to render_template('search/_results')
+ expect(view.page_description).to match(/issues results for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path)
end
- context 'unfurling support' do
- let(:group) { build(:group) }
- let(:search_results) do
- instance_double(Gitlab::GroupSearchResults).tap do |double|
- allow(double).to receive(:formatted_count).and_return(0)
- end
- end
-
- before do
- assign(:search_results, search_results)
- assign(:scope, 'issues')
- assign(:group, group)
- end
-
- context 'search with full count' do
- let(:search_service_presenter) do
- instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
- end
-
- it 'renders meta tags for a group' do
- render
-
- expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
- expect(view.page_card_attributes).to eq("Namespace" => group.full_path)
- end
-
- it 'renders meta tags for both group and project' do
- project = build(:project, group: group)
- assign(:project, project)
-
- render
-
- expect(view.page_description).to match(/\d+ issues for term '#{search_term}'/)
- expect(view.page_card_attributes).to eq("Namespace" => group.full_path, "Project" => project.full_path)
- end
- end
-
- context 'search without full count' do
- let(:search_service_presenter) do
- instance_double(SearchServicePresenter, without_count?: true, advanced_search_enabled?: false)
- end
-
- it 'renders meta tags for a group' do
- render
-
- expect(view.page_description).to match(/issues results for term '#{search_term}'/)
- expect(view.page_card_attributes).to eq("Namespace" => group.full_path)
- end
-
- it 'renders meta tags for both group and project' do
- project = build(:project, group: group)
- assign(:project, project)
-
- render
-
- expect(view.page_description).to match(/issues results for term '#{search_term}'/)
- expect(view.page_card_attributes).to eq("Namespace" => group.full_path, "Project" => project.full_path)
- end
- end
+ it 'renders meta tags for both group and project' do
+ project = build(:project, group: group)
+ assign(:project, project)
+
+ render
+
+ expect(view.page_description).to match(/issues results for term '#{search_term}'/)
+ expect(view.page_card_attributes).to eq("Namespace" => group.full_path, "Project" => project.full_path)
end
end
end
diff --git a/spec/views/shared/runners/_runner_details.html.haml_spec.rb b/spec/views/shared/runners/_runner_details.html.haml_spec.rb
index 978750c8435..6e95f6e8075 100644
--- a/spec/views/shared/runners/_runner_details.html.haml_spec.rb
+++ b/spec/views/shared/runners/_runner_details.html.haml_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'shared/runners/_runner_details.html.haml' do
context 'when runner is of type project' do
let(:runner) { create(:ci_runner, :project) }
- it { is_expected.to have_content("Runner ##{runner.id} specific") }
+ it { is_expected.to have_content("Runner ##{runner.id} project") }
end
end
diff --git a/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb b/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb
index c9bdcabb4b6..5cef3a949d3 100644
--- a/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb
+++ b/spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb
@@ -2,21 +2,9 @@
require 'spec_helper'
RSpec.describe 'shared/ssh_keys/_key_delete.html.haml' do
- context 'when the icon parameter is used' do
- it 'has text' do
- render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { icon: true, button_data: '' }
+ it 'has text' do
+ render partial: 'shared/ssh_keys/key_delete', formats: :html, locals: { button_data: '' }
- expect(rendered).not_to have_button('Delete')
- expect(rendered).to have_selector('[data-testid=remove-icon]')
- end
- end
-
- 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: { button_data: '' }
-
- expect(rendered).to have_button('Delete')
- expect(rendered).not_to have_selector('[data-testid=remove-icon]')
- end
+ expect(rendered).to have_button('Delete')
end
end
diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
index 0e7b657a154..821112e12bc 100644
--- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
@@ -52,8 +52,8 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
before do
assign(:sidebar_wiki_entries, create_list(:wiki_page, 3, wiki: wiki))
assign(:sidebar_limited, true)
- stub_template "../shared/wikis/_wiki_pages.html.erb" => "Entries: <%= @sidebar_wiki_entries.size %>"
- stub_template "../shared/wikis/_wiki_page.html.erb" => 'A WIKI PAGE'
+ stub_template "shared/wikis/_wiki_pages.html.erb" => "Entries: <%= @sidebar_wiki_entries.size %>"
+ stub_template "shared/wikis/_wiki_page.html.erb" => 'A WIKI PAGE'
end
it 'does not show an alert' do
@@ -66,7 +66,7 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
it 'renders the wiki content' do
render
- expect(rendered).to include('A WIKI PAGE' * 3)
+ expect(rendered).to include("A WIKI PAGE\n" * 3)
expect(rendered).to have_link('View All Pages')
end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 61c33f123fa..ec8550bb3bc 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImportWorker do
+RSpec.describe BulkImportWorker, feature_category: :importers do
describe '#perform' do
context 'when no bulk import is found' do
it 'does nothing' do
@@ -56,6 +56,19 @@ RSpec.describe BulkImportWorker do
end
end
+ context 'when maximum allowed number of import entities in progress' do
+ it 'reenqueues itself' do
+ bulk_import = create(:bulk_import, :started)
+ create(:bulk_import_entity, :created, bulk_import: bulk_import)
+ (described_class::DEFAULT_BATCH_SIZE + 1).times { |_| create(:bulk_import_entity, :started, bulk_import: bulk_import) }
+
+ expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id)
+ expect(BulkImports::ExportRequestWorker).not_to receive(:perform_async)
+
+ subject.perform(bulk_import.id)
+ end
+ end
+
context 'when bulk import is created' do
it 'marks bulk import as started' do
bulk_import = create(:bulk_import, :created)
@@ -82,16 +95,20 @@ RSpec.describe BulkImportWorker do
context 'when there are created entities to process' do
let_it_be(:bulk_import) { create(:bulk_import, :created) }
- it 'marks all entities as started, enqueues EntityWorker, ExportRequestWorker and reenqueues' do
+ before do
+ stub_const("#{described_class}::DEFAULT_BATCH_SIZE", 1)
+ end
+
+ it 'marks a batch of entities as started, enqueues EntityWorker, ExportRequestWorker and reenqueues' do
create(:bulk_import_entity, :created, bulk_import: bulk_import)
create(:bulk_import_entity, :created, bulk_import: bulk_import)
expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id)
- expect(BulkImports::ExportRequestWorker).to receive(:perform_async).twice
+ expect(BulkImports::ExportRequestWorker).to receive(:perform_async).once
subject.perform(bulk_import.id)
- expect(bulk_import.entities.map(&:status_name)).to contain_exactly(:started, :started)
+ expect(bulk_import.entities.map(&:status_name)).to contain_exactly(:created, :started)
end
context 'when there are project entities to process' do
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index 03ec6267ca8..e8b0714471d 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -433,11 +433,11 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
allow(status).to receive(:failed?).and_return(false)
end
- entity.update!(created_at: entity_created_at)
+ pipeline_tracker.update!(created_at: created_at)
end
context 'when timeout is not reached' do
- let(:entity_created_at) { 1.minute.ago }
+ let(:created_at) { 1.minute.ago }
it 'reenqueues pipeline worker' do
expect(described_class)
@@ -455,8 +455,8 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
end
end
- context 'when timeout is reached' do
- let(:entity_created_at) { 10.minutes.ago }
+ context 'when empty export timeout is reached' do
+ let(:created_at) { 10.minutes.ago }
it 'marks as failed and logs the error' do
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
@@ -485,12 +485,32 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(pipeline_tracker.reload.status_name).to eq(:failed)
end
end
+
+ context 'when tracker created_at is nil' do
+ let(:created_at) { nil }
+
+ it 'falls back to entity created_at' do
+ entity.update!(created_at: 10.minutes.ago)
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(
+ hash_including('exception.message' => 'Empty export status on source instance')
+ )
+ end
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ end
+ end
end
context 'when job reaches timeout' do
it 'marks as failed and logs the error' do
- old_created_at = entity.created_at
- entity.update!(created_at: (BulkImports::Pipeline::NDJSON_EXPORT_TIMEOUT + 1.hour).ago)
+ old_created_at = pipeline_tracker.created_at
+ pipeline_tracker.update!(created_at: (BulkImports::Pipeline::NDJSON_EXPORT_TIMEOUT + 1.hour).ago)
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger)
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 14abe819587..0c1010960a1 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ArchiveTracesCronWorker do
+RSpec.describe Ci::ArchiveTracesCronWorker, feature_category: :continuous_integration do
subject { described_class.new.perform }
let(:finished_at) { 1.day.ago }
@@ -34,14 +34,28 @@ RSpec.describe Ci::ArchiveTracesCronWorker do
it_behaves_like 'archives trace'
- it 'executes service' do
+ it 'batch_execute service' do
expect_next_instance_of(Ci::ArchiveTraceService) do |instance|
- expect(instance).to receive(:execute).with(build, anything)
+ expect(instance).to receive(:batch_execute).with(worker_name: "Ci::ArchiveTracesCronWorker")
end
subject
end
+ context "with FF deduplicate_archive_traces_cron_worker false" do
+ before do
+ stub_feature_flags(deduplicate_archive_traces_cron_worker: false)
+ end
+
+ it 'calls execute service' do
+ expect_next_instance_of(Ci::ArchiveTraceService) do |instance|
+ expect(instance).to receive(:execute).with(build, worker_name: "Ci::ArchiveTracesCronWorker")
+ end
+
+ subject
+ end
+ end
+
context 'when the job finished recently' do
let(:finished_at) { 1.hour.ago }
diff --git a/spec/workers/ci/cancel_redundant_pipelines_worker_spec.rb b/spec/workers/ci/cancel_redundant_pipelines_worker_spec.rb
new file mode 100644
index 00000000000..f6639faab10
--- /dev/null
+++ b/spec/workers/ci/cancel_redundant_pipelines_worker_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CancelRedundantPipelinesWorker, feature_category: :continuous_integration do
+ let_it_be(:project) { create(:project) }
+
+ let(:prev_pipeline) { create(:ci_pipeline, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ describe '#perform' do
+ subject(:perform) { described_class.new.perform(pipeline.id) }
+
+ let(:service) { instance_double('Ci::PipelineCreation::CancelRedundantPipelinesService') }
+
+ it 'calls cancel redundant pipeline service' do
+ expect(Ci::PipelineCreation::CancelRedundantPipelinesService)
+ .to receive(:new)
+ .with(pipeline)
+ .and_return(service)
+
+ expect(service).to receive(:execute)
+
+ perform
+ end
+
+ context 'if pipeline is deleted' do
+ subject(:perform) { described_class.new.perform(non_existing_record_id) }
+
+ it 'does not call redundant pipeline service' do
+ expect(Ci::PipelineCreation::CancelRedundantPipelinesService)
+ .not_to receive(:new)
+
+ perform
+ end
+ end
+
+ describe 'interacting with previous pending pipelines', :sidekiq_inline do
+ before do
+ create(:ci_build, :interruptible, :running, pipeline: prev_pipeline)
+ end
+
+ it_behaves_like 'an idempotent worker', :sidekiq_inline do
+ let(:job_args) { pipeline }
+
+ it 'cancels the previous pending pipeline' do
+ perform
+
+ expect(prev_pipeline.builds.pluck(:status)).to contain_exactly('canceled')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb b/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
index a637ac088ff..1f20729a821 100644
--- a/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
+++ b/spec/workers/ci/external_pull_requests/create_pipeline_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::ExternalPullRequests::CreatePipelineWorker do
+RSpec.describe Ci::ExternalPullRequests::CreatePipelineWorker, feature_category: :continuous_integration do
let_it_be(:project) { create(:project, :auto_devops, :repository) }
let_it_be(:user) { project.first_owner }
let_it_be(:external_pull_request) do
diff --git a/spec/workers/ci/runners/stale_machines_cleanup_cron_worker_spec.rb b/spec/workers/ci/runners/stale_machines_cleanup_cron_worker_spec.rb
new file mode 100644
index 00000000000..d8f620bc024
--- /dev/null
+++ b/spec/workers/ci/runners/stale_machines_cleanup_cron_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Runners::StaleMachinesCleanupCronWorker, feature_category: :runner_fleet do
+ let(:worker) { described_class.new }
+
+ describe '#perform', :freeze_time do
+ subject(:perform) { worker.perform }
+
+ let!(:runner_machine1) do
+ create(:ci_runner_machine, created_at: 7.days.ago, contacted_at: 7.days.ago)
+ end
+
+ let!(:runner_machine2) { create(:ci_runner_machine) }
+ let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.days.ago) }
+
+ it_behaves_like 'an idempotent worker' do
+ it 'delegates to Ci::Runners::StaleMachinesCleanupService' do
+ expect_next_instance_of(Ci::Runners::StaleMachinesCleanupService) do |service|
+ expect(service)
+ .to receive(:execute).and_call_original
+ end
+
+ perform
+
+ expect(worker.logging_extras).to eq({
+ "extra.ci_runners_stale_machines_cleanup_cron_worker.status" => :success,
+ "extra.ci_runners_stale_machines_cleanup_cron_worker.deleted_machines" => true
+ })
+ end
+
+ it 'cleans up stale runner machines', :aggregate_failures do
+ expect(Ci::RunnerMachine.stale.count).to eq 1
+
+ expect { perform }.to change { Ci::RunnerMachine.count }.from(3).to(2)
+
+ expect(Ci::RunnerMachine.all).to match_array [runner_machine2, runner_machine3]
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 5fde54b98f0..0abb029f146 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -103,6 +103,15 @@ RSpec.describe ApplicationWorker do
expect(instance.logging_extras).to eq({ 'extra.gitlab_foo_bar_dummy_worker.key1' => "value1", 'extra.gitlab_foo_bar_dummy_worker.key2' => "value2" })
end
+ it 'returns extra data to be logged that was set from #log_hash_metadata_on_done' do
+ instance.log_hash_metadata_on_done({ key1: 'value0', key2: 'value1' })
+
+ expect(instance.logging_extras).to match_array({
+ 'extra.gitlab_foo_bar_dummy_worker.key1' => 'value0',
+ 'extra.gitlab_foo_bar_dummy_worker.key2' => 'value1'
+ })
+ end
+
context 'when nothing is set' do
it 'returns {}' do
expect(instance.logging_extras).to eq({})
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index 1449c327052..737424ffd8c 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -22,38 +22,6 @@ RSpec.describe WaitableWorker do
subject(:job) { worker.new }
- describe '.bulk_perform_and_wait' do
- context '1 job' do
- it 'runs the jobs asynchronously' do
- arguments = [[1]]
-
- expect(worker).to receive(:bulk_perform_async).with(arguments)
-
- worker.bulk_perform_and_wait(arguments)
- end
- end
-
- context 'between 2 and 3 jobs' do
- it 'runs the jobs asynchronously' do
- arguments = [[1], [2], [3]]
-
- expect(worker).to receive(:bulk_perform_async).with(arguments)
-
- worker.bulk_perform_and_wait(arguments)
- end
- end
-
- context '>= 4 jobs' do
- it 'runs jobs using sidekiq' do
- arguments = 1.upto(5).map { |i| [i] }
-
- expect(worker).to receive(:bulk_perform_async).with(arguments)
-
- worker.bulk_perform_and_wait(arguments)
- end
- 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/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index c444e1f383c..c0d46a206ce 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -303,6 +303,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::JiraImport::Stage::StartImportWorker' => 5,
'Gitlab::PhabricatorImport::ImportTasksWorker' => 5,
'GitlabPerformanceBarStatsWorker' => 3,
+ 'GitlabSubscriptions::RefreshSeatsWorker' => 0,
'GitlabShellWorker' => 3,
'GitlabServicePingWorker' => 3,
'GroupDestroyWorker' => 3,
@@ -363,6 +364,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Onboarding::PipelineCreatedWorker' => 3,
'Onboarding::ProgressWorker' => 3,
'Onboarding::UserAddedWorker' => 3,
+ 'Namespaces::FreeUserCap::OverLimitNotificationWorker' => false,
'Namespaces::RefreshRootStatisticsWorker' => 3,
'Namespaces::RootStatisticsWorker' => 3,
'Namespaces::ScheduleAggregationWorker' => 3,
diff --git a/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
index c0b32515d15..bdafc076465 100644
--- a/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
+++ b/spec/workers/google_cloud/fetch_google_ip_list_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GoogleCloud::FetchGoogleIpListWorker do
+RSpec.describe GoogleCloud::FetchGoogleIpListWorker, feature_category: :build_artifacts do
describe '#perform' do
it 'returns success' do
allow_next_instance_of(GoogleCloud::FetchGoogleIpListService) do |service|
diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb
index c96bb4a3d1e..145ee780573 100644
--- a/spec/workers/incident_management/close_incident_worker_spec.rb
+++ b/spec/workers/incident_management/close_incident_worker_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker do
let(:issue_id) { issue.id }
it 'calls the close issue service' do
- expect_next_instance_of(Issues::CloseService, project: project, current_user: user) do |service|
+ expect_next_instance_of(Issues::CloseService, container: project, current_user: user) do |service|
expect(service).to receive(:execute).with(issue, system_note: false).and_call_original
end
diff --git a/spec/workers/issues/close_worker_spec.rb b/spec/workers/issues/close_worker_spec.rb
index 41611447db1..3902618ae03 100644
--- a/spec/workers/issues/close_worker_spec.rb
+++ b/spec/workers/issues/close_worker_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Issues::CloseWorker do
external_issue = ExternalIssue.new("foo", project)
closer = instance_double(Issues::CloseService, execute: true)
- expect(Issues::CloseService).to receive(:new).with(project: project, current_user: user).and_return(closer)
+ expect(Issues::CloseService).to receive(:new).with(container: project, current_user: user).and_return(closer)
expect(closer).to receive(:execute).with(external_issue, commit: commit)
worker.perform(project.id, external_issue.id, external_issue.class.to_s, opts)
diff --git a/spec/workers/merge_requests/close_issue_worker_spec.rb b/spec/workers/merge_requests/close_issue_worker_spec.rb
index 5e6bdc2a43e..72fb3be7470 100644
--- a/spec/workers/merge_requests/close_issue_worker_spec.rb
+++ b/spec/workers/merge_requests/close_issue_worker_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe MergeRequests::CloseIssueWorker do
let!(:merge_request) { create(:merge_request, source_project: project) }
it 'calls the close issue service' do
- expect_next_instance_of(Issues::CloseService, project: project, current_user: user) do |service|
+ expect_next_instance_of(Issues::CloseService, container: project, current_user: user) do |service|
expect(service).to receive(:execute).with(issue, commit: merge_request)
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 a7e4ffad259..e17ad02e272 100644
--- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
+++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
@@ -13,114 +13,53 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker 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)
+ 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(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(non_existing_record_id, sha, user.id)
- end
- end
+ context 'with a non-existing merge request' do
+ it 'does nothing' do
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- context 'with a non-existing user' do
- it 'does nothing' do
- expect(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(merge_request.id, sha, non_existing_record_id)
- end
+ worker.perform(non_existing_record_id, sha, user.id)
end
+ end
- context 'with existing user and merge request' do
- it 'creates a new delete branch worker async' do
- 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(::Projects::DeleteBranchWorker).not_to receive(:new)
-
- worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
- end
- end
- end
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- it_behaves_like 'an idempotent worker' do
- let(:job_args) { [merge_request.id, sha, user.id] }
+ worker.perform(merge_request.id, sha, non_existing_record_id)
end
end
- context 'when the add_delete_branch_worker feature flag is disabled' do
- before do
- stub_feature_flags(add_delete_branch_worker: false)
- end
-
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
-
- worker.perform(non_existing_record_id, sha, user.id)
+ context 'with existing user and merge request' do
+ it 'calls delete branch worker' do
+ 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 'with a non-existing user' do
+ context 'source branch sha does not match' do
it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+ expect(::Projects::DeleteBranchWorker).not_to receive(:new)
- worker.perform(merge_request.id, sha, non_existing_record_id)
+ worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
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(merge_request.source_branch)
- end
+ context 'when delete worker raises an error' do
+ it 'still retargets the merge request' do
+ expect(::Projects::DeleteBranchWorker).to receive(:new).and_raise(StandardError)
- worker.perform(merge_request.id, sha, user.id)
- end
-
- it 'calls service to try retarget merge requests' do
expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
expect(instance).to receive(:execute).with(merge_request)
end
- worker.perform(merge_request.id, sha, user.id)
- end
-
- context 'source branch sha does not match' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
-
- worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
- end
- end
-
- context 'when delete service returns an error' do
- let(:service_result) { ServiceResponse.error(message: 'placeholder') }
-
- it 'still retargets the merge request' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result)
- end
-
- expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
- expect(instance).to receive(:execute).with(merge_request)
- end
-
- worker.perform(merge_request.id, sha, user.id)
- end
+ expect { worker.perform(merge_request.id, sha, user.id) }.to raise_error(StandardError)
end
end
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
index 358939a963a..a8e1c3f4bf1 100644
--- a/spec/workers/new_merge_request_worker_spec.rb
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NewMergeRequestWorker do
+RSpec.describe NewMergeRequestWorker, feature_category: :code_review_workflow do
describe '#perform' do
let(:worker) { described_class.new }
@@ -71,19 +71,64 @@ RSpec.describe NewMergeRequestWorker do
it_behaves_like 'a new merge request where the author cannot trigger notifications'
end
- context 'when everything is ok' do
+ include_examples 'an idempotent worker' do
let(:user) { create(:user) }
-
- it 'creates a new event record' do
- expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
- end
-
- it 'creates a notification for the mentioned user' do
- expect(Notify).to receive(:new_merge_request_email)
- .with(mentioned.id, merge_request.id, NotificationReason::MENTIONED)
- .and_return(double(deliver_later: true))
-
- worker.perform(merge_request.id, user.id)
+ let(:job_args) { [merge_request.id, user.id] }
+
+ context 'when everything is ok' do
+ it 'creates a new event record' do
+ expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
+ end
+
+ it 'creates a notification for the mentioned user' do
+ expect(Notify).to receive(:new_merge_request_email)
+ .with(mentioned.id, merge_request.id, NotificationReason::MENTIONED)
+ .and_return(double(deliver_later: true))
+
+ worker.perform(merge_request.id, user.id)
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is off' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: false)
+ end
+
+ it 'calls the create service' do
+ expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+
+ context 'when add_prepared_state_to_mr feature flag is on' do
+ before do
+ stub_feature_flags(add_prepared_state_to_mr: true)
+ end
+
+ context 'when the merge request is prepared' do
+ before do
+ merge_request.update!(prepared_at: Time.current)
+ end
+
+ it 'does not call the create service' do
+ expect(MergeRequests::AfterCreateService).not_to receive(:new)
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+
+ context 'when the merge request is not prepared' do
+ it 'calls the create service' do
+ expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+ end
end
end
end
diff --git a/spec/workers/packages/debian/generate_distribution_worker_spec.rb b/spec/workers/packages/debian/generate_distribution_worker_spec.rb
index a3e956f14c8..c4e974ec8eb 100644
--- a/spec/workers/packages/debian/generate_distribution_worker_spec.rb
+++ b/spec/workers/packages/debian/generate_distribution_worker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker do
+RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker, feature_category: :package_registry do
describe '#perform' do
let(:container_type) { distribution.container_type }
let(:distribution_id) { distribution.id }
@@ -18,12 +18,6 @@ RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker do
context "for #{container_type}" do
include_context 'with Debian distribution', container_type
- 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 mocked service' do
it 'calls GenerateDistributionService' do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
diff --git a/spec/workers/packages/debian/process_changes_worker_spec.rb b/spec/workers/packages/debian/process_changes_worker_spec.rb
index fc482245ebe..b96b75e93b9 100644
--- a/spec/workers/packages/debian/process_changes_worker_spec.rb
+++ b/spec/workers/packages/debian/process_changes_worker_spec.rb
@@ -2,12 +2,14 @@
require 'spec_helper'
-RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker do
+RSpec.describe Packages::Debian::ProcessChangesWorker, 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_it_be_with_reload(:distribution) do
+ create(:debian_project_distribution, :with_file, codename: FFaker::Lorem.word, suite: 'unstable')
+ end
let(:incoming) { create(:debian_incoming, project: distribution.project) }
- let(:package_file) { incoming.package_files.last }
+ let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first }
let(:worker) { described_class.new }
describe '#perform' do
@@ -16,12 +18,6 @@ RSpec.describe Packages::Debian::ProcessChangesWorker, type: :worker do
subject { worker.perform(package_file_id, user_id) }
- 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 mocked service' do
it 'calls ProcessChangesService' do
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
diff --git a/spec/workers/packages/debian/process_package_file_worker_spec.rb b/spec/workers/packages/debian/process_package_file_worker_spec.rb
index 532bfb096a3..239ee8e1035 100644
--- a/spec/workers/packages/debian/process_package_file_worker_spec.rb
+++ b/spec/workers/packages/debian/process_package_file_worker_spec.rb
@@ -3,18 +3,19 @@
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_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file) }
+ let_it_be_with_reload(:package) do
+ create(:debian_package, :processing, project: distribution.project, published_in: nil)
+ end
- let(:incoming) { create(:debian_incoming, project: distribution.project) }
let(:distribution_name) { distribution.codename }
+ let(:debian_file_metadatum) { package_file.debian_file_metadatum }
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) }
+ subject { worker.perform(package_file_id, distribution_name, component_name) }
shared_examples 'returns early without error' do
it 'returns early without error' do
@@ -34,7 +35,7 @@ RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, featur
with_them do
context 'with Debian package file' do
- let(:package_file) { incoming.package_files.with_file_name(file_name).first }
+ let(:package_file) { package.package_files.with_file_name(file_name).first }
context 'with mocked service' do
it 'calls ProcessPackageFileService' do
@@ -48,57 +49,44 @@ RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, featur
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 }
+ let(:package_file) { package.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first }
- it 'removes package file', :aggregate_failures do
+ it 'marks the package as errored', :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)
+ .and not_change { Packages::PackageFile.count }
+ .and not_change { package.package_files.count }
+ .and change { package.reload.status }.from('processing').to('error')
end
end
it_behaves_like 'an idempotent worker' do
- let(:job_args) { [package_file.id, user.id, distribution_name, component_name] }
+ let(:job_args) { [package_file.id, distribution_name, component_name] }
it 'sets the Debian file type as deb', :aggregate_failures do
+ expect(::Packages::Debian::GenerateDistributionWorker)
+ .to receive(:perform_async).with(:project, distribution.id)
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)
+ .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&.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
+ .and change { Packages::Debian::Publication.count }.by(1)
+ .and not_change(package.package_files, :count)
+ .and change { package.reload.name }.to('sample')
+ .and change { package.version }.to('1.2.3~alpha2')
+ .and change { package.status }.from('processing').to('default')
+ .and change { package.debian_publication }.from(nil)
+ .and change { debian_file_metadatum.reload.file_type }.from('unknown').to(expected_file_type)
+ .and change { debian_file_metadatum.component }.from(nil).to(component_name)
end
end
end
@@ -113,15 +101,9 @@ RSpec.describe Packages::Debian::ProcessPackageFileWorker, type: :worker, featur
end
context 'with a deb' do
- let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first }
+ let(:package_file) { package.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 }
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index db58dc00338..da6a0254a17 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -49,19 +49,7 @@ RSpec.describe PipelineScheduleWorker, :sidekiq_inline, feature_category: :conti
end
end
- shared_examples 'successful scheduling with/without ci_use_run_pipeline_schedule_worker' do
- it_behaves_like 'successful scheduling'
-
- context 'when feature flag ci_use_run_pipeline_schedule_worker is disabled' do
- before do
- stub_feature_flags(ci_use_run_pipeline_schedule_worker: false)
- end
-
- it_behaves_like 'successful scheduling'
- end
- end
-
- it_behaves_like 'successful scheduling with/without ci_use_run_pipeline_schedule_worker'
+ it_behaves_like 'successful scheduling'
context 'when the latest commit contains [ci skip]' do
before do
@@ -70,7 +58,7 @@ RSpec.describe PipelineScheduleWorker, :sidekiq_inline, feature_category: :conti
.and_return('some commit [ci skip]')
end
- it_behaves_like 'successful scheduling with/without ci_use_run_pipeline_schedule_worker'
+ it_behaves_like 'successful scheduling'
end
end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 072c660bc2b..143809e8f2a 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -137,7 +137,7 @@ RSpec.describe ProcessCommitWorker do
context 'when issue has no first_mentioned_in_commit_at set' do
it 'updates issue metrics' do
- expect(update_metrics_and_reload)
+ expect { update_metrics_and_reload.call }
.to change { issue.metrics.first_mentioned_in_commit_at }.to(commit.committed_date)
end
end
@@ -148,7 +148,7 @@ RSpec.describe ProcessCommitWorker do
end
it "doesn't update issue metrics" do
- expect(update_metrics_and_reload).not_to change { issue.metrics.first_mentioned_in_commit_at }
+ expect { update_metrics_and_reload.call }.not_to change { issue.metrics.first_mentioned_in_commit_at }
end
end
@@ -158,7 +158,7 @@ RSpec.describe ProcessCommitWorker do
end
it "doesn't update issue metrics" do
- expect(update_metrics_and_reload)
+ expect { update_metrics_and_reload.call }
.to change { issue.metrics.first_mentioned_in_commit_at }.to(commit.committed_date)
end
end
diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb
index 732dc540fb7..b702eed9ea4 100644
--- a/spec/workers/projects/post_creation_worker_spec.rb
+++ b/spec/workers/projects/post_creation_worker_spec.rb
@@ -93,13 +93,10 @@ RSpec.describe Projects::PostCreationWorker do
context 'when project is created', :aggregate_failures do
it 'creates tags for the project' do
- expect { subject }.to change { IncidentManagement::TimelineEventTag.count }.by(2)
+ expect { subject }.to change { IncidentManagement::TimelineEventTag.count }.by(6)
expect(project.incident_management_timeline_event_tags.pluck_names).to match_array(
- [
- ::IncidentManagement::TimelineEventTag::START_TIME_TAG_NAME,
- ::IncidentManagement::TimelineEventTag::END_TIME_TAG_NAME
- ]
+ ::IncidentManagement::TimelineEventTag::PREDEFINED_TAGS
)
end
diff --git a/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb b/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
index 00c45255316..99627ff1ad2 100644
--- a/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
+++ b/spec/workers/projects/refresh_build_artifacts_size_statistics_worker_spec.rb
@@ -17,12 +17,14 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsWorker do
build(
:project_build_artifacts_size_refresh,
:running,
+ id: 99,
project_id: 77,
last_job_artifact_id: 123
)
end
it 'logs refresh information' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:refresh_id, refresh.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:project_id, refresh.project_id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:last_job_artifact_id, refresh.last_job_artifact_id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:last_batch, refresh.destroyed?)
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 25158de3341..75938d3b793 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -21,11 +21,6 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
end
end
- it 'accepts an option' do
- expect { worker.perform(pipeline_schedule.id, user.id, {}) }.not_to raise_error
- expect { worker.perform(pipeline_schedule.id, user.id, {}, {}) }.to raise_error(ArgumentError)
- end
-
context 'when a schedule not found' do
it 'does not call the Service' do
expect(Ci::CreatePipelineService).not_to receive(:new)
@@ -60,6 +55,7 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
describe "#run_pipeline_schedule" do
let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService, execute: service_response) }
let(:service_response) { instance_double(ServiceResponse, payload: pipeline, error?: false) }
+ let(:pipeline) { instance_double(Ci::Pipeline, persisted?: true) }
context 'when pipeline can be created' do
before do
@@ -69,8 +65,6 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
end
context "when pipeline is persisted" do
- let(:pipeline) { instance_double(Ci::Pipeline, persisted?: true) }
-
it "returns the service response" do
expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response)
end
@@ -81,17 +75,23 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response)
end
- it "changes the next_run_at" do
- expect { worker.perform(pipeline_schedule.id, user.id) }.to change { pipeline_schedule.reload.next_run_at }.by(1.day)
+ it "does not change the next_run_at" do
+ expect { worker.perform(pipeline_schedule.id, user.id) }.not_to change { pipeline_schedule.reload.next_run_at }
end
- context 'when feature flag ci_use_run_pipeline_schedule_worker is disabled' do
- before do
- stub_feature_flags(ci_use_run_pipeline_schedule_worker: false)
+ context 'when scheduling option is given as true' do
+ it "returns the service response" do
+ expect(worker.perform(pipeline_schedule.id, user.id, scheduling: true)).to eq(service_response)
+ end
+
+ it "does not log errors" do
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ expect(worker.perform(pipeline_schedule.id, user.id, scheduling: true)).to eq(service_response)
end
- it 'does not change the next_run_at' do
- expect { worker.perform(pipeline_schedule.id, user.id) }.not_to change { pipeline_schedule.reload.next_run_at }
+ it "changes the next_run_at" do
+ expect { worker.perform(pipeline_schedule.id, user.id, scheduling: true) }.to change { pipeline_schedule.reload.next_run_at }.by(1.day)
end
end
end
@@ -122,31 +122,12 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
expect { worker.perform(pipeline_schedule.id, user.id) }.to not_change { pipeline_schedule.reload.next_run_at }
end
- it 'does not create a pipeline' do
- expect(Ci::CreatePipelineService).not_to receive(:new)
+ it 'creates a pipeline' do
+ expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
+ expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
worker.perform(pipeline_schedule.id, user.id)
end
-
- context 'when feature flag ci_use_run_pipeline_schedule_worker is disabled' do
- let(:pipeline) { instance_double(Ci::Pipeline, persisted?: true) }
-
- before do
- stub_feature_flags(ci_use_run_pipeline_schedule_worker: false)
-
- expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
-
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
- end
-
- it 'does not change the next_run_at' do
- expect { worker.perform(pipeline_schedule.id, user.id) }.to not_change { pipeline_schedule.reload.next_run_at }
- end
-
- it "returns the service response" do
- expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response)
- end
- end
end
end
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 e884a71933e..c3c3612f9a7 100644
--- a/spec/workers/tasks_to_be_done/create_worker_spec.rb
+++ b/spec/workers/tasks_to_be_done/create_worker_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe TasksToBeDone::CreateWorker do
expect(service_class)
.to receive(:new)
- .with(project: member_task.project, current_user: current_user, assignee_ids: assignee_ids)
+ .with(container: member_task.project, current_user: current_user, assignee_ids: assignee_ids)
.and_call_original
end